More Effective C# 29.避免結合同步與非同步方法 More Effective C# 29.避免結合同步與非同步方法(Avoid Composing Synchronous and Asynchronous Methods)

這個做法提到同步方法與非同步方法根本上的行為差異,所以不要寫出混用這兩種方法的程式碼。

一個帶有 async 定義的非同步方法就代表這個方法可能在完成所有工作之前就回傳,回傳的任務物件可能處於 TaskStatus 這個 Enum 中的一個狀態, async 定義的非同步方法也可能代表這個指定的任務需要更多的時間才能完成,所以呼叫者為了避免浪費時間,建議是在等待期間同時執行其他工作。

一個同步方法則是執行時會阻塞呼叫者的操作,因為他們是共用同一個資源,直到完成所有工作。

將這兩種執行方式混和在一起的設計可能會導致錯誤、死鎖、消耗過多資源等問題,所以能得出兩個重要的結論,第一個是不要在非同步方法中 建立同步方法因為會造成阻塞,第二個是避免在非同步方法中觸發長時間運行的 CPU bound 工作。

以下分別是非同步與同步方法呼叫 async 方法 GetLeftOperandForIndex 並取得結果的流程,可以看到同步方法使用了 ResultWait 來獲取結果,所以攔截方法的時候需要攔截 AggregateException 並自行篩選出需要的錯誤,非同步方法則不需要這種處理方法,它會自動拋出第一個 紀錄在 AggregateException 的錯誤,所以不需要額外處理。

public static async Task<int> ComputeUsageAsync()
{
	try
	{
		var operand = await GetLeftOperandForIndex(19);
		var operand2 = await GetRightOperandForIndex(23);
		return operand + operand2;
	}
	catch (KeyNotFoundException e)
	{
		return 0;
	}
}
public static int ComputeUsage()
{
	try
	{
		var operand = GetLeftOperandForIndex(19).Result;
		var operand2 = GetRightOperandForIndex(23).Result;
		return operand + operand2;
	}
	catch (AggregateException e)
	when (e.InnerExceptions.FirstOrDefault().GetType() == typeof(KeyNotFoundException))
	{
		return 0;
	}
}

下面這種寫法在 ASP.NET 的時代會造成死鎖,是因為 SynchronizationContext 只包含一條執行緒,導致 await 與 wait 兩邊在互相等待造成死鎖, 但這個問題在 .NET Core 並不會發生。

private static async Task SimulatedWorkAsync()
{
	await Task.Delay(1000);
}
public static void SyncOverAsyncDeadlock()
{
	var delayTask = SimulatedWorkAsync();
	delayTask.Wait();
}

另外不見使用同步方法(例如 Thread.Sleep)等待非同步操作,因為它會持續佔用執行緒資源,建議改用非同步的 Task.Delay 。

Thread.Sleep(1000); // 阻塞執行緒,資源浪費
await Task.Delay(1000); // 釋放執行緒,提升效率

最後是不要將 CPU 密集型操作單純包裝成非同步方法,例如呼叫下面這個 ComputeValue 方法能夠讓呼叫者自行決定要直接以同步的方式運行,還是建立另一個執行緒 以非同步的方式運行,但如果呼叫的是 ComputeValueAsync 方法因為它只提供了包裝的功能所以會到 thread pool 拿一條或新增一條執行緒, 像這種 CPU 密集型操作是否要跑在新的執行緒上應該要留給呼叫者自行決定,而不應該由模組決定。

public double ComputeValue()
{
	double finalAnswer = 0;
	for (int i = 0; i < 10_000_000; i++)
		finalAnswer += InterimCalculation(i);
	return finalAnswer;
}

public Task<double> ComputeValueAsync()
{
	return Task.Run(() => ComputeValue());
}

Summary

這個做法警告不要在一個非同步方法混用同步方法,因為可能會造成死鎖或效能以及捕捉錯誤寫法不同等問題,還有不要在函式庫提供一個非同步的包裝方法, 應該要把這個決定權交給呼叫者自行決定比較好。