這個做法主要在介紹使用 ValueTask 與 ValueTask
到目前為止我們寫的非同步方法的回傳值基本上都是 Task
或是 Task<T>
,但在某些時刻 Task 型別可能會造成性能瓶頸,例如在 loop
每次迴圈都建立新的 Task
,就會導致大量的 heap allocate,同時也會增加 GC 回收的次數與壓力。
由於新版本的 C# 沒有 async 強迫一定要用 Task
或 Task<T>
做為回傳值,現在只要物件有提供 GetAwaiter
方法並
有實做 INotifyCompletion
或 ICriticalNotifyCompletion
就能符合需求,所以就有 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
這個做法建議在某些對時間不敏感的資料可以額外設計緩存的機制,避免影響應用程式性能,另外建議平常使用 Task
或 Task<T>
即可,
除非真的測量到 allocate memory 真的是性能瓶頸才改用 ValueTask
。