﻿# 釐清非同步程式設計模型下 Task 與 ValueTask 的使用時機

在 .NET 的世界裡，從 .NET Framework 4.0 問世之前，你只能使用 Thread、APM ([Asynchronous Programming Model](https://learn.microsoft.com/zh-tw/dotnet/standard/asynchronous-programming-patterns/asynchronous-programming-model-apm)) 或 EAP ([Event-based Asynchronous Pattern](https://learn.microsoft.com/zh-tw/dotnet/standard/asynchronous-programming-patterns/event-based-asynchronous-pattern-eap)) 開發非同步程式碼，其實對不熟悉非同步開發的人來說，是有一點小小的進入門檻。但從 .NET Framework 4.0 開始推出的 TAP ([Task-based Asynchronous Pattern](https://learn.microsoft.com/zh-tw/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap)) 這種以 Task 為基礎的非同步模式，不但可以透過 async/await 大幅簡化非同步開發的思維模式，還能產生更容易閱讀、好維護的程式碼。今天這篇文章，我將介紹 .NET Core 2.0 搭配 C# 7.0 推出的一個 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 結構，並說明他跟 [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 類別的不同之處！

![](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/88a6e66ca879bc38c095-170986781-e13b34fc-a6b3-4c1a-acf7-169747b1c521.png)

> 注意: [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 只能用在 .NET Core / .NET 的應用程式中，若你用 .NET Framework 是找不到這個型別的。

### 效能差異

首先，這兩個型別有個本質上的差異：

1.  [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 類別
    
    所有的**類別**(class)都是一種[參考型別](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/reference-types?WT.mc_id=DT-MVP-4015686) ([Reference Type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types?WT.mc_id=DT-MVP-4015686))，這意味著，當你在執行一個標示 `async` 的非同步方法時，若該方法會透過 `Task<TResult>` 物件立即回應一個**值**，你就必須先在 Heap 記憶體中先將 Task 物件保存，然後再將該物件的記憶體位址參考放入 Stack 記憶體中，而最後**保存在變數中的資料**其實是該物件的**記憶體位址參考**。因此，你要透過 Task 物件取得執行結果時，會有一點點點點的額外開銷(overhead)。
    
2.  [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 結構
    
    所有的**結構**(struct)都是一種[實質型別](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/builtin-types/value-types?WT.mc_id=DT-MVP-4015686) ([Value Type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types?WT.mc_id=DT-MVP-4015686))，這意味著，當你在執行一個標示 `async` 的非同步方法時，若該方法會透過 `ValueTask<TResult>` 物件立即回應一個**值**，由於是**實質型別**的關係，這個 `ValueTask<TResult>` 物件會直接儲存在 Stack 記憶體中進行操作。簡單來說，就是記憶體操作的效率比較好！
    

有這麼好的東西，怎麼不用爆？正所謂**理想很豐滿，現實很骨感**，為了能夠理解「理想」與「現實」的差距，我們肯定要來效能比較一下！

我使用 [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) 來評比一下彼此之間的效能差異：

```csharp
#LINQPad optimize+     // Enable compiler optimizations

void Main()
{
    Util.AutoScrollResults = true;
    BenchmarkRunner.Run<TaskAndValueTaskComparsion>();
}

[ShortRunJob]
public class TaskAndValueTaskComparsion
{
    [Benchmark]
    public ValueTask<int> RunValueTaskWithNew()
    {
        return new ValueTask<int>(1);
    }

    [Benchmark]
    public Task<int> RunTaskFromResult()
    {
        return Task.FromResult(1);
    }
}
```

結果效能評比的結果竟然是 `Task<int>` 幾乎完勝 `ValueTask<int>` 耶，意思也就是說，使用 `ValueTask<int>` 並沒有比較快啊！🔥

![BenchmarkDotNet: ValueTask vs. Task](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/6c88d1f806bf772019e7-170973597-dbdc9cdb-ea3f-4e79-85b2-e501913253e8.jpg)

驚不驚喜？意不意外？😆

老實說，我認真覺得要學好**非同步程式開發**不是一件很容易的事，在非同步的世界裡，有太多的變因(或迷因?)，有時候會因為硬體環境、軟體設定不同，就會有截然不同的執行結果，以致於許多人在研讀龐大文件時，經常無法有效的吸收知識，更嚴重的就是建立了錯誤的觀念而不自知。在多執行緒的執行環境下，有很多觀念需要事先建立，你才有辦法好好的、正確的思考，也才有辦法舉一反三，做出正確的非同步設計決策。也因此我設計了 [C# 開發實戰：非同步程式開發技巧](https://www.accupass.com/event/2202110214416242167820) 這堂課程，用兩天的時間，幫助你徹底搞懂 .NET 的非同步程式開發技巧！👍

我從 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 官方文件看到以下說明：

> As such, the default choice for any asynchronous method should be to return a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task) or [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1). Only if performance analysis proves it worthwhile should a [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) be used instead of a [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1). The non generic version of [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) is not recommended for most scenarios. The [CompletedTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.completedtask#system-threading-tasks-task-completedtask) property should be used to hand back a successfully completed singleton in the case where a method returning a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task) completes synchronously and successfully.

簡單翻譯一下重點知識：

1.  所有非同步方法都應該使用 [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task) 或 [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 當作非同步方法的預設回傳型別。
    
    > 所以並不是什麼程式都可以用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 啦！
    
2.  除了有明確的數據證明用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 的效能比 [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task) 好，你才去用，否則不要！
    
    > 換句話說，背後原理很複雜，我不想跟你說這麼多，真的有數據證明效能 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 比較快你才考慮用看看。
    
3.  非泛型的 [ValueTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask) 在大多數情境下都是不建議使用的。
    
    > 雖然還是有個 [ValueTask.CompletedTask](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.completedtask#system-threading-tasks-task-completedtask) 屬性可用，但你只會用在只想設定非同步工作完成的情境。
    

好吧！回歸正題！為什麼 `Task<int>` 幾乎完勝 `ValueTask<int>` 呢？這不合理啊！沒錯，確實不合理，我們來看一下 [Task.FromResult](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs#L5219-L5272) 的原始碼，當設定非同步結果的物件屬於 **實質型別** (Value Type) 的時候，他會對一些**常見的數值**設定 Task 快取 ([TaskCache.cs](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskCache.cs))，所以上述的效能評比並不公平，因為 [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 的結果完全都被快取起來了！🔥

> 當使用 `Task.FromResult()` 的時候，預設 Int32 的結果只要介於 `-1` ~ `9` 之間，都會自動回應快取的 Task 版本！🔥

我們重新修改一下測試程式：

```csharp
#LINQPad optimize+     // Enable compiler optimizations

void Main()
{
    Util.AutoScrollResults = true;
    BenchmarkRunner.Run<TaskAndValueTaskComparsion>();
}

[ShortRunJob]
public class TaskAndValueTaskComparsion
{
    [Benchmark]
    public ValueTask<int> RunValueTaskWithNew()
    {
        return new ValueTask<int>(10);
    }

    [Benchmark]
    public Task<int> RunTaskFromResult()
    {
        return Task.FromResult(10);
    }
}
```

評測結果如下：

![BenchmarkDotNet: ValueTask vs. Task](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f93a6165c693fcf012b3-170976082-c3de234c-8944-4825-9aba-0491ee6e0f20.png)

這個結果終於沒那麼毀人三觀了！ 😄🤟

### 使用 ValueTask 的限制

使用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 有許多限制條件，只要你無法接受這些限制，建議就不要用了：

1.  你不能在一個 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 物件上使用 `await` 等待兩次以上！
    
2.  你不能在一個 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 物件上使用 `.AsTask()` 兩次以上！
    
3.  如果你的 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 還沒有跑出結果就執行 `.Result` 或 `.GetAwaiter().GetResult()` 的話，是會掛掉的！
    
    > 因為 `IValueTaskSource` / `IValueTaskSource<TResult>` 的實作不支援 blocking 操作，因此不能在 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 尚未完成之前進行任何 blocking 的操作。
    
4.  如果你的 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 執行了兩次以上 `.Result` 或 `.GetAwaiter().GetResult()` 的話，是會掛掉的！
    
5.  上述所有的使用方式只要用超過一次都會掛掉！
    

上述任何一點限制你無法接受，就不要用！

### 使用 ValueTask 的時機

以下是我個人認為使用的 `ValueTask` 時機點，僅供大家參考：

1.  如果你可以接受 `ValueTask` 的使用限制，才考慮用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 來實現，否則就用 [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 即可。
    
2.  如果你的非同步方法可以預期的很快會執行完畢，可以考慮用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 來實現。
    
3.  如果你的**非同步方法**中可能會混用「同步」與「非同步」的執行模式，也意味著在「同步」執行時為了擁有更好的效率，那麼可以考慮用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1)，但請記得做好效能分析，讓數據說話。
    
    這裡有個很簡單的例子如下：
    
    ```csharp
    public async Task<bool> MoveNextAsync()
    {
        if (_bufferedCount == 0)
        {
            await FillBuffer();
        }
        return _bufferedCount > 0;
    }
    ```
    
    這個 `MoveNextAsync()` 非同步方法，在沒有 Buffer 的時候，會需要執行另一個非同步方法，但是在有 Buffer 的時候，卻可以直接回傳結果 (同步執行)。這種情境下，最適合將非同步方法改用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 來回傳，記憶體使用率較佳，且執行速度也較快！
    
    ```csharp
    public async ValueTask<bool> MoveNextAsync()
    {
        if (_bufferedCount == 0)
        {
            await FillBuffer();
        }
        return _bufferedCount > 0;
    }
    ```
    
4.  如果你在一個物件中有一個或多個 `Task<TResult>` 欄位(Field)時，你在變數之間複製的時候，若改用 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 將會導致複製過多的資料，會比較耗用記憶體，所以使用上需要考量一下。老話一句，讓數據說話，效能評測過再說。
    
    由於 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 屬於「實質型別」，因此你沒辦法快取這個物件，如果你想快取 [ValueTask<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1) 的話，可以考慮使用 [ValueTask<TResult>.AsTask()](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1.astask) 方法，將物件轉回 [Task<TResult>](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1) 型別，**參考型別**就很容易快取了！
    

### 使用 `new ValueTask<T>()` 或 `ValueTask.FromResult<T>()`

我分析了一下 [dotnet/aspnetcore](https://github.com/dotnet/aspnetcore/) 的所有原始碼，我發現這兩種語法都用蠻多的：

-   `new ValueTask<T>()`
    
    ```csharp
    public ValueTask<ChannelReader<int>> StreamChannelReaderValueTaskAsync()
    {
        var channel = Channel.CreateUnbounded<int>();
        channel.Writer.Complete();
    
        return new ValueTask<ChannelReader<int>>(channel);
    }
    ```
    
-   `ValueTask.FromResult<T>()`
    
    ```csharp
    public ValueTask<ChannelReader<int>> StreamChannelReaderValueTaskAsync()
    {
        var channel = Channel.CreateUnbounded<int>();
        channel.Writer.Complete();
    
        return ValueTask.FromResult<ChannelReader<int>>(channel);
    }
    ```
    

我也去查了一下 [ValueTask.cs L119](https://source.dot.net/#System.Private.CoreLib/ValueTask.cs,119) 原始碼，這兩種寫法根本沒有差異，但是用 `ValueTask.FromResult<T>()` 我覺得可讀性比較好，但用 `new ValueTask<T>()` 可以少一次 Method Call 方法呼叫。客官就自己選擇吧，我自己是選 `ValueTask.FromResult<T>()` 啦！ 🤟

```csharp
/// <summary>Creates a <see cref="ValueTask{TResult}"/> that's completed successfully with the specified result.</summary>
/// <typeparam name="TResult">The type of the result returned by the task.</typeparam>
/// <param name="result">The result to store into the completed task.</param>
/// <returns>The successfully completed task.</returns>
public static ValueTask<TResult> FromResult<TResult>(TResult result) =>
    new ValueTask<TResult>(result);
```

### 相關連結

-   [Understanding the Whys, Whats, and Whens of ValueTask - .NET Blog](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/?WT.mc_id=DT-MVP-4015686)
-   [Task<TResult> Class (System.Threading.Tasks) | Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-   [ValueTask<TResult> Struct (System.Threading.Tasks) | Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1)
-   [C# Language Tutorial => ValueTask<T>](https://riptutorial.com/csharp/example/28612/valuetask-t-)
-   [c# - Is there a ContinueWith for ValueTask? - Stack Overflow](https://stackoverflow.com/a/61424368/910074)
-   [C# Language Highlights: ValueTask](https://learn.microsoft.com/en-us/shows/on-net/c-language-highlights-valuetask?WT.mc_id=DT-MVP-4015686) (Video)
-   [When to use ValueTask instead of Task and save precious memory in C# - YouTube](https://www.youtube.com/watch?v=mEhkelf0K6g)
-   [Task vs ValueTask: When Should I use ValueTask? - Brian Lagunas](https://brianlagunas.com/task-vs-valuetask-when-should-i-use-valuetask/)
