More Effective C# 34.緩衝擴充的非同步回傳值 More Effective C# 34.緩衝擴充的非同步回傳值(Cache Generalized Async Return types)

這個做法主要在介紹使用 ValueTask 與 ValueTask 好處與使用的時機。

到目前為止我們寫的非同步方法的回傳值基本上都是 Task 或是 Task<T>,但在某些時刻 Task 型別可能會造成性能瓶頸,例如在 loop 每次迴圈都建立新的 Task,就會導致大量的 heap allocate,同時也會增加 GC 回收的次數與壓力。

由於新版本的 C# 沒有 async 強迫一定要用 TaskTask<T> 做為回傳值,現在只要物件有提供 GetAwaiter 方法並 有實做 INotifyCompletionICriticalNotifyCompletion 就能符合需求,所以就有 ValueTask 這個值型別的任務出現了。

舉例來說,下面這個方法能夠取得指定日期的氣象資料,可以看到這個方法每次呼叫都要連到網路獲取資料,由於資料改變的速度並不會這麼快 所以每次都獲取新資料很沒有效率。

public async Task<IEnumerable<WeatherData>> RetrieveHistoricalData(DateTime start, DateTime end)
{
	var observationDate = start;
	var results = new List<WeatherData>();

	while (observationDate < end)
	{
		var observation = await RetrieveObservationData(observationDate);
		results.Add(observation);
		observationDate += TimeSpan.FromDays(1);
	}

	return results;
}

要優化這個問題可以使用緩存機制,限制五分鐘以內的資料直接讀取緩存資料即可。

private List<WeatherData> recentObservations = new List<WeatherData>();
private DateTime lastReading;
public async Task<IEnumerable<WeatherData>> RetrieveHistoricalData()
{
	if (DateTime.Now - lastReading > TimeSpan.FromMinutes(5))
	{
		recentObservations = new List<WeatherData>();
		var observationDate = this.startDate;
		while (observationDate < this.endDate)
		{
			var observation = await RetrieveObservationData(observationDate);
			recentObservations.Add(observation);
			observationDate += TimeSpan.FromDays(1);
		}
		lastReading = DateTime.Now;
	}
	return recentObservations;
}

但如果運行應用程式的機器有限制記憶體,上面的寫法就不太適合了,因為每次呼叫方法都會建立 Task 並分配記憶體,所以可以改用 ValueTask 進一步改善這個問題。 首先要注意到這個方法並不是常見的 async 方法,它是透過 nested function 來執行非同步的工作,這樣可以避免在判斷時間是否讀取緩存時, 不用在額外建立一層狀態機。 另外 ValueTask 提供了一個建構函式能夠傳入 Task 物件,並且內部會自動 await 這個物件。

public ValueTask<IEnumerable<WeatherData>> RetrieveHistoricalData()
{
	if (DateTime.Now - lastReading > TimeSpan.FromMinutes(5))
	{
		return new ValueTask<IEnumerable<WeatherData>>(recentObservations);
	}
	else
	{
		async Task<IEnumerable<WeatherData>> loadCache()
		{
			recentObservations = new List<WeatherData>();
			var observationDate = this.startDate;
			while (observationDate < this.endDate)
			{
				var observation = await RetrieveObservationData(observationDate);
				recentObservations.Add(observation);
				observationDate += TimeSpan.FromDays(1);
			}
			lastReading = DateTime.Now;
			return recentObservations;
		}
		return new ValueTask<IEnumerable<WeatherData>>(loadCache());
	}
}

Summary

這個做法建議在某些對時間不敏感的資料可以額外設計緩存的機制,避免影響應用程式性能,另外建議平常使用 TaskTask<T> 即可, 除非真的測量到 allocate memory 真的是性能瓶頸才改用 ValueTask