如何使用 C# 中的 ValueTask

愛喝啤酒的雷神發表於2020-11-06

在 C# 中利用  ValueTask 避免從非同步方法返回  Task 物件時分配

翻譯自 Joydip Kanjilal 2020年7月6日 的文章 
非同步程式設計已經使用了相當長一段時間了。近年來,隨著  async 和  await 關鍵字的引入,它變得更加強大。您可以利用非同步程式設計來提高應用程式的響應能力和吞吐量。

C# 中非同步方法的推薦返回型別是  Task。如果您想編寫一個有返回值的非同步方法,那麼應該返回  Task<T>; 如果想編寫事件處理程式,則可以返回  void。在 C# 7.0 之前,非同步方法可以返回  TaskTask<T> 或  void。從 C# 7.0 開始,非同步方法還可以返回  ValueTask(作為  System.Threading.Tasks.Extensions 包的一部分可用)或  ValueTask<T>。本文就討論一下如何在 C# 中使用  ValueTask

要使用本文提供的程式碼示例,您的系統中需要安裝 Visual Studio 2019。如果還沒有安裝,您可以 。

在 Visual Studio 中建立一個 .NET Core 控制檯應用程式專案

首先,讓我們在 Visual Studio 中建立一個 .NET Core 控制檯應用程式專案。假設您的系統中安裝了 Visual Studio 2019,請按照下面描述的步驟在 Visual Studio 中建立一個新的 .NET Core 控制檯應用程式專案。
  1. 啟動 Visual Studio IDE。
  2. 點選 “建立新專案”。
  3. 在 “建立新專案” 視窗中,從顯示的模板列表中選擇 “控制檯應用(.NET Core)”。
  4. 點選 “下一步”。
  5. 在接下來顯示的 “配置新專案” 視窗,指定新專案的名稱和位置。
  6. 點選 “建立”。
這將在 Visual Studio 2019 中建立一個新的 .NET Core 控制檯應用程式專案。我們將在本文後面的部分中使用這個專案來說明  ValueTask 的用法。

為什麼要使用 ValueTask ?

Task 表示某個操作的狀態,即此操作是否完成、取消等。非同步方法可以返回  Task 或者  ValueTask

現在,由於  Task 是一個引用型別,從非同步方法返回一個  Task 物件意味著每次呼叫該方法時都會在託管堆( managed heap)上分配該物件。因此,在使用  Task 時需要注意的一點是,每次從方法返回  Task 物件時都需要在託管堆中分配記憶體。如果你的方法執行的操作的結果立即可用或同步完成,則不需要這種分配,因此代價很高。

這正是  ValueTask 要出手相助的目的, ValueTask<T> 提供了兩個主要好處。首先, ValueTask<T> 提高了效能,因為它不需要在堆( heap)中分配; 其次,它的實現既簡單又靈活。當結果立即可用時,透過從非同步方法返回  ValueTask<T> 代替  Task<T>,你可以避免不必要的分配開銷,因為這裡的 “T” 表示一個結構,而 C# 中的結構體( struct)是一個值型別(與  Task<T> 中表示類的 “T” 不同)。

C# 中  Task 和  ValueTask 表示兩種主要的 “可等待(awaitable)” 型別。請注意,您不能阻塞(block)一個  ValueTask。如果需要阻塞,則應使用  AsTask 方法將  ValueTask 轉換為  Task,然後在該引用  Task 物件上進行阻塞。

另外請注意,每個  ValueTask 只能被消費(consumed)一次。這裡的單詞 “消費(consume)” 是指  ValueTask 可以非同步等待( await)操作完成,或者利用  AsTask 將  ValueTask 轉換為  Task。但是, ValueTask 只應被消費(consumed)一次,之後  ValueTask<T> 應被忽略。

C# 中的 ValueTask 示例

假設有一個非同步方法返回一個  Task。你可以利用  Task.FromResult 建立  Task 物件,如下面給出的程式碼片段所示。
public Task<int> GetCustomerIdAsync() {     return Task.FromResult(1); }
上面的程式碼片段並沒有建立整個非同步狀態機制,但它在託管堆( managed heap)中分配了一個  Task 物件。為了避免這種分配,您可能希望利用  ValueTask 代替,像下面給出的程式碼片段所示的那樣。
public ValueTask<int> GetCustomerIdAsync() {     return new ValueTask<int>(1); }
下面的程式碼片段演示了  ValueTask 的同步實現。
public interface IRepository<T> {     ValueTask<T> GetData(); }
Repository 類擴充套件了  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(); }
現在讓我們將另一個方法新增到我們的儲存庫(repository)中,這次是一個名為  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 意味著要處理更多的資料,因為方法呼叫將返回兩個資料欄位而不是一個。另外,如果您等待( await)一個返回  ValueTask 的方法,那麼該非同步方法的狀態機也會更大,因為它必須容納一個包含兩個欄位的結構體而不是在使用  Task 時的單個引用。

此外,如果非同步方法的使用者使用  Task.WhenAll 或者  Task.WhenAny,在非同步方法中使用  ValueTask<T> 作為返回型別可能會代價很高。這是因為您需要使用  AsTask 方法將  ValueTask<T> 轉換為  Task<T>,這會引發一個分配,而如果使用起初快取的  Task<T>,則可以輕鬆避免這種分配。

經驗法則是這樣的:當您有一段程式碼總是非同步的時,即當操作(總是)不能立即完成時,請使用  Task。當非同步操作的結果已經可用時,或者當您已經快取了結果時,請利用  ValueTask。不管怎樣,在考慮使用  ValueTask 之前,您都應該執行必要的效能分析。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984138/viewspace-2732620/,如需轉載,請註明出處,否則將追究法律責任。

相關文章