在 C# 中利用 ValueTask 避免從異步方法返回 Task 對象時分配
翻譯自 Joydip Kanjilal 2020年7月6日 的文章 《How to use ValueTask in C#》
異步編程已經使用了相當長一段時間了。近年來,隨着 async 和 await 關鍵字的引入,它變得更加強大。您可以利用異步編程來提高應用程序的響應能力和吞吐量。
C# 中異步方法的推薦返回類型是 Task。如果您想編寫一個有返回值的異步方法,那么應該返回 Task<T>; 如果想編寫事件處理程序,則可以返回 void。在 C# 7.0 之前,異步方法可以返回 Task、Task<T> 或 void。從 C# 7.0 開始,異步方法還可以返回 ValueTask(作為 System.Threading.Tasks.Extensions 包的一部分可用)或 ValueTask<T>。本文就討論一下如何在 C# 中使用 ValueTask。
要使用本文提供的代碼示例,您的系統中需要安裝 Visual Studio 2019。如果還沒有安裝,您可以在這里下載 Visual Studio 2019。
在 Visual Studio 中創建一個 .NET Core 控制台應用程序項目
首先,讓我們在 Visual Studio 中創建一個 .NET Core 控制台應用程序項目。假設您的系統中安裝了 Visual Studio 2019,請按照下面描述的步驟在 Visual Studio 中創建一個新的 .NET Core 控制台應用程序項目。
- 啟動 Visual Studio IDE。
- 點擊 “創建新項目”。
- 在 “創建新項目” 窗口中,從顯示的模板列表中選擇 “控制台應用(.NET Core)”。
- 點擊 “下一步”。
- 在接下來顯示的 “配置新項目” 窗口,指定新項目的名稱和位置。
- 點擊 “創建”。
這將在 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 之前,您都應該執行必要的性能分析。
ValueTask是readonly struct類型,Task是class類型。
相關鏈接:C# 中 Struct 和 Class 的區別總結。
作者 : Joydip Kanjilal
譯者 : 技術譯民
出品 : 技術譯站
鏈接 : 英文原文
