在 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
譯者 : 技術譯民
出品 : 技術譯站
鏈接 : 英文原文