More Effective C# 31.避免非必要的封送處理 (Marshalling) Context More Effective C# 31.避免非必要的封送處理 (Marshalling) Context(Avoid Marshalling Context Unnecessarily)

這個做法主要在講解 context、context-free、context-aware 的概念,以及 ConfigureAwait 背後的原理。

首先定義 context-free 程式碼代表這段程式碼不必依賴 context 就能順利執行,context-aware 程式碼則是必須在特定的 context 才能順利執行, 所以你在隨意一個 context 運行 context-free 程式碼那麼不會發生什麼嚴重問題,但你在隨意一個 context 運行 context-aware 程式碼可能會發生嚴重錯誤。

會討論這個的原因是在前幾篇的做法中有提到,非同步方法有中斷與恢復的功能,那麼這個中斷會捕捉當時執行的 context 以後在恢復的時候理論上就可以避免 context switch 的發生避免影響性能,但實際上有些時候 context switch 反而會比不執行 context switch 性能來要好。

所以就有了 ConfigureAwait() 這個方法,它的用途是通知非同步方法不用在捕捉的 context 執行剩餘的程式碼, 設定為 false 就能讓剩餘的程式碼在新的 context 中執行,當然要確定剩餘的程式碼是 context-free 程式碼。

例如下面這段程式碼在非同步方法的後面加上 ConfigureAwait(false),通知剩餘的程式碼不用在捕捉的 context 執行剩餘的程式碼。

public static async Task<XElement> ReadPacket(string Url)
{
	var result = await DownloadAsync(Url).ConfigureAwait(false);
	return XElement.Parse(result);
}

接下來看看這段方法,你可能會認為 DownloadAsync 有設定 ConfigureAwait(false) 那麼之後的程式碼就不會強迫運行在捕捉的 context 中, 但實際上如果 DownloadAsync 執行的速度過快會讓整段程式碼以同步的方式運行,那其實就不會產生 context switch 也就是會一直運行在原始的 context 中, 要避免這個問題最好是把所有非同步方法都加上 ConfigureAwait(false)

public static async Task<Config> ReadConfig(string Url)
{
	var result = await DownloadAsync(Url).ConfigureAwait(false);
	var items = XElement.Parse(result);
	var userConfig = from node in items.Descendants()
					 where node.Name == "Config"
					 select node.Value;
	var configUrl = userConfig.SingleOrDefault();
	if (configUrl != null)
	{
		result = await DownloadAsync(configUrl).ConfigureAwait(false);
		var config = await ParseConfig(result).ConfigureAwait(false);
		return config;
	}
	else
		return new Config();
}

這個做法的困難點就是知道自己寫的程式碼是不是 context-aware 的,因為只有 context-aware 的程式碼才有必要運行在捕捉的 context 中, 另外一但離開捕捉的 context 那就沒辦法再回去了,通常只有更新 UI 的程式碼是 context-aware 的,應該把其它的程式碼都設定 ConfigureAwait(false)

像這段程式碼就是混合了 UI 更新的程式碼,就應該把 context-free 設定 ConfigureAwait(false),UI 更新的程式碼保持預設。

private async void OnCommand(object sender, RoutedEventArgs e)
{
	var viewModel = (DataContext as SampleViewModel);
	try
	{
		Config config = await ReadConfigAsync(viewModel);
		await viewModel.Update(config);
	}
	catch (Exception ex) when (logMessage(viewModel, ex))
	{
	}
}

private async Task<Config> ReadConfigAsync(SampleViewModel viewModel)
{
	var userInput = viewModel.webSite;
	var result = await DownloadAsync(userInput).ConfigureAwait(false);
	var items = XElement.Parse(result);
	var userConfig = from node in items.Descendants()
					 where node.Name == "Config"
					 select node.Value;
	var configUrl = userConfig.SingleOrDefault();
	var config = default(Config);
	if (configUrl != null)
	{
		result = await DownloadAsync(configUrl).ConfigureAwait(false);
		config = await ParseConfig(result).ConfigureAwait(false);
	}
	else
		config = new Config();
	return config;
}

Summary

這個做法在新的 .NET Core 不會有影響,因為在已經沒有 SynchronizationContext 所以設定 ConfigureAwait(false) 並沒有影響, 但可能你的函式庫會與 ASP.NET 共用,所以一般還是建議都是加上 ConfigureAwait(false)