概要
1、如果異步方法的使用者使用 Task.WhenAll 或 Task.WhenAny,則在異步方法中使用 ValueTask<T> 作為返回類型可能會產生高昂的成本。這是因為您需要使用 AsTask 方法將 ValueTask<T> 轉換為 Task<T>這將產生一個分配,如果首先使用了緩存的 Task<T>,則可以輕松避免這種分配
2、每個值任務只能使用一次。此處的"消費"一詞意味着 ValueTask 可以異步等待(等待)操作完成,或者利用 AsTask 將 ValueTask 轉換為任務。但是,一個值任務只應使用一次,之后應忽略值任務<T>。
如何在 C 中使用 ValueTask#
利用 C# 中的 ValueTask,避免在從異步方法返回任務對象時進行分配
異步編程已經使用了相當長一段時間。近年來,隨着異步和等待關鍵字的引入,它變得更加強大。您可以利用異步編程來提高應用程序的響應能力和吞吐量。
C# 中異步方法的建議返回類型是 Task。如果要編寫返回值的異步方法,則應返回 Task<T>。如果要編寫事件處理程序,可以改為返回 void。在 C# 7.0 之前,異步方法可以返回 Task、Task<T> 或 void。從 C# 7.0 開始,異步方法還可以返回 ValueTask(作為 System.Threading.Tasks.Extensions 包的一部分提供)或 ValueTask<T>。本文討論了如何在 C# 中使用 ValueTask。
為什么我應該使用 ValueTask?
任務表示某個操作的狀態,即操作是否已完成、是否取消等。異步方法可以返回 Task 或 ValueTask。
現在,由於 Task 是引用類型,因此從異步方法返回 Task 對象意味着每次調用該方法時都要在托管堆上分配該對象。因此,使用 Task 時需要注意的一點是,每次從方法返回 Task 對象時,都需要在托管堆中分配內存。如果方法正在執行的操作的結果立即可用或同步完成,則不需要此分配,因此成本會變得高昂。
這正是ValueTask的救星。ValueTask<T>提供了兩個主要優點。首先,ValueTask<T>提高了性能,因為它不需要堆分配,其次,它既簡單又靈活地實現。當結果立即可用時,通過從異步方法返回 ValueTask<T> 而不是 Task<T>,可以避免不必要的分配開銷,因為此處的"T"表示結構,而 C# 中的結構是值類型(與 Task<T> 中的"T"相反,后者表示類)。
Task 和 ValueTask 表示 C# 中的兩種主要的"可等待"類型。請注意,您無法阻止值任務。如果需要阻止,則應使用 AsTask 方法將 ValueTask 轉換為任務,然后阻止該引用 Task 對象。
另請注意,每個值任務只能使用一次。此處的"消費"一詞意味着 ValueTask 可以異步等待(等待)操作完成,或者利用 AsTask 將 ValueTask 轉換為任務。但是,一個值任務只應使用一次,之后應忽略值任務<T>。
C# 中的值任務示例
假設您有一個返回 Task 的異步方法。您可以利用 Task.FromResult 創建 Task 對象,如下面給出的代碼片段所示。
public Task<int> GetCustomerIdAsync() { return Task.FromResult(1); }
上面的代碼片段不會創建整個異步狀態機魔術,但它會在托管堆中分配一個 Task 對象。若要避免此分配,您可能希望改為利用 ValueTask,如下面給出的代碼段所示。
public ValueTask<int> GetCustomerIdAsync() { return new ValueTask(1); }
以下代碼段闡釋了 ValueTask 的同步實現。
public interface IRepository<T> { ValueTask<T> GetData(); }
存儲庫類擴展 IRepository 接口並實現其方法,如下所示。
public class Repository<T> : IRepository<T> { public ValueTask<T> GetData() { var value = default(T); return new ValueTask<T>(value); } }
下面介紹了如何從 Main 方法調用 GetData 方法。
static void Main(string[] args) { IRepository<int> repository = new Repository<int>(); var result = repository.GetData(); if(result.IsCompleted) Console.WriteLine("Operation complete..."); else Console.WriteLine("Operation incomplete..."); Console.ReadKey(); }
現在,讓我們向存儲庫中添加另一個方法,這次是名為 GetDataAsync 的異步方法。以下是修改后的 IRepository 接口的外觀。
public interface IRepository<T> { ValueTask<T> GetData(); ValueTask<T> GetDataAsync(); }
GetDataAsync 方法由 Repository 類實現,如下面給出的代碼片段所示。
public class Repository<T> : IRepository<T> { public ValueTask<T> GetData() { var value = default(T); return new ValueTask<T>(value); } public async ValueTask<T> GetDataAsync() { var value = default(T); await Task.Delay(100); return value; } }
何時應在 C# 中使用 ValueTask?
盡管 ValueTask 提供了許多好處,但使用 ValueTask 代替 Task 需要一些權衡。ValueTask 是具有兩個字段的值類型,而 Task 是具有單個字段的引用類型。因此,使用 ValueTask 意味着處理更多數據,因為方法調用將返回兩個數據字段而不是一個字段。此外,如果等待返回 ValueTask 的方法,則該異步方法的狀態機也會更大,因為在 Task 的情況下,它必須容納包含兩個字段的結構,而不是單個引用。
此外,如果異步方法的使用者使用 Task.WhenAll 或 Task.WhenAny,則在異步方法中使用 ValueTask<T> 作為返回類型可能會產生高昂的成本。這是因為您需要使用 AsTask 方法將 ValueTask<T> 轉換為 Task<T>這將產生一個分配,如果首先使用了緩存的 Task<T>,則可以輕松避免這種分配。
這是經驗法則。當您有一段始終是異步的代碼時,即當操作不會立即完成時,請使用 Task。當異步操作的結果已經可用或已有緩存的結果時,請利用 ValueTask。無論哪種方式,您都應該在考慮 ValueTask 之前執行必要的性能分析。