CodeGym /課程 /C# SELF /效能優化: ValueTask

效能優化: ValueTask

C# SELF
等級 62, 課堂 2
開放

1. 介紹

在動手寫 code 前,先用白話講清楚。記得在 .NET 裡建立一個物件(更別說一個 task)是要花記憶體跟一些 CPU 時間的嗎?現在想像一個非同步 API,半數情況下結果是立刻可用的(例如從快取拿),另一半情況下要去查資料庫,所以操作就變成真正的非同步,需要 Task。這種情況很常見——像檔案快取、網路 API、物件池以及其他那些「非同步但有時瞬間完成」的場景。

如果我們總是回傳 Task,即便結果是即時的,也得去建立不必要的物件。要是可以在結果已就緒時直接回傳結果而不要 Task 呢?這就是 ValueTask 誕生的理由。

事實: 標準的 Task.CompletedTask 和 Task.FromResult(…) 確實能節省執行時間,但它們會建立共享物件,對高負載場景不一定理想。

什麼是 ValueTask

ValueTask 是一個特殊的 struct 包裝器,它可以表示已就緒的結果,或者(如果操作是真正非同步的)包含一個 Task。簡單說,它是個「包裹」,裡面要麼是直接結果,要麼是指向 Task 的參考。

主要有兩種變體:

  • ValueTask — 無回傳值(像 Task)
  • ValueTask<TResult> — 包裝值(類似 Task<TResult>)

比較 Task 與 ValueTask

類型 分配次數 是否可同步完成 通常用在
Task
一次(heap) 是/否 幾乎總是
ValueTask
零次/一次 是/否 優化用
ValueTask<T>
零次/一次 是/否 優化用

何時應用 ValueTask

有條黃金法則:如果你寫的非同步方法總是在某個 async 操作之後才回傳結果,就用 Task。簡單、安全,大家都懂。

當以下情況適合用 ValueTask:

  • 結果可能同步可得(例如從快取、物件池或記憶體拿),你想省掉不必要的分配。
  • 真正需要非同步的情況不常發生(否則代價會被結構複製與程式複雜度吃掉)。

注意! 如果你總是回傳非同步結果——大膽用 Task。如果想讓 API 在常見的瞬時結果下「超優化」——考慮用 ValueTask。

2. 同步或非同步結果

看一個尋找使用者的函式範例。若使用者在快取裡就立刻回傳;若不在則從「資料庫」非同步載入:


// 模擬我們的使用者
public class User
{
    public string Name { get; set; }
}

// 我們的 cache(非常簡單)
private readonly Dictionary<string, User> _localCache = new();

public async ValueTask<User> FindUserAsync(string name)
{
    // 檢查本地快取
    if (_localCache.TryGetValue(name, out var user))
    {
        // 結果是同步的 — 不會分配 Task!
        return user;
    }

    // 這裡是假設的慢的非同步操作(例如從資料庫)
    user = await LoadUserFromDbAsync(name);
    // 放進快取以備未來使用
    _localCache[name] = user;
    return user;
}

private async Task<User> LoadUserFromDbAsync(string name)
{
    // 模擬延遲
    await Task.Delay(500);
    return new User { Name = name };
}

重要: 在快取命中時我們回傳的是普通結果——沒有 Task 分配!只有在必須「取得」結果時,才會建立非同步工作。

ValueTask 在內部是怎麼運作的

ValueTask 內部可能包含已就緒的值,或是一個 Task 的參考:

ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));

使用 await 時,編譯器會自動處理:如果結果是即時的,就不會建立多餘的物件。

關於 await 與 ValueTask 的重要事項

async 方法可以很好地與 await 和 ValueTask 搭配:

public async ValueTask PingAsync()
{
    // ...
    await Task.Delay(10);
}

但如果你把 ValueTask 實例儲存起來,想晚點再等它,請記住:不能 對同一個實例做多次 await。Task 可以被重複等待,ValueTask 則不行。

錯誤:對 ValueTask 重複 await

ValueTask<int> task = ComputeAsync();

// 這是可以的
int a = await task;

// 這是錯誤!同一個 ValueTask 實例不允許第二次 await:
// int b = await task; // 不行!

3. 重寫我們的 console 應用部分

假設我們正在做一個簡單的電子書閱讀器:部分文本已在快取、其餘的會非同步下載。用 ValueTask 實作一個取得書籍第一行的最佳化方法:

private readonly Dictionary<string, string> _bookCache = new();

public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
    if (_bookCache.TryGetValue(title, out var bookText))
    {
        var firstLine = bookText.Split('\n')[0];
        return firstLine;
    }

    // 假設這裡是非同步下載書籍
    var downloadedBook = await DownloadBookTextAsync(title);
    _bookCache[title] = downloadedBook;
    return downloadedBook.Split('\n')[0];
}

private async Task<string> DownloadBookTextAsync(string title)
{
    // 模擬延遲(例如從網路下載)
    await Task.Delay(1000);
    return $"Book: {title}\nThis is the first line.\nSecond line...";
}

這樣一來,當書已在快取時,ValueTask 可以省掉建立 Task。

4. 有用的細節

提示:何時不該使用 ValueTask

如果你的 API 永遠是非同步的(例如總是跟網路打交道)——就用 Task,比較簡單也更安全。

如何把 ValueTask 轉成 Task,以及反過來

有時 ValueTask 無法直接傳給需要 Task 的 API,這時用 .AsTask():

ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();

若要從 Task 產生 ValueTask — 直接用 Task 建構 ValueTask:

Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);

比較:ValueTask vs Task

特性
Task
ValueTask
重複 await 允許 不允許
快速回應時的分配 會有 沒有(若結果即時)
相容性介面 廣泛支援 需要額外包裝
適用場景 各處適用 僅作為優化
Pool(物件池) 使用共用池 沒有,為 struct
簡單度 簡單 較複雜

在現實中使用 ValueTask

public async ValueTask<string> GetMessageAsync(int id)
{
    if (_messageCache.TryGetValue(id, out var value))
        return value;

    var result = await LoadMessageFromDbAsync(id);
    _messageCache[id] = result;
    return result;
}

面試應答要點: 如果被問到如何進一步提升帶有快取的非同步 API 的效能——提到 ValueTask。

與 LINQ 及 IAsyncEnumerable<T> 的相容範例

想把 ValueTask 用在非同步 LINQ(IAsyncEnumerable<T>)時,.NET 是支援的(例如 ToListAsync 等方法):

public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
    // 假設部分數字已計算,部分是非同步產生
    // ...(範例為簡短省略)
    return new List<int> { 2, 3, 5, 7, 11 };
}

5. 結論與實作要點

  1. 當大量時間內結果是立即可用(例如來自快取)時,使用 ValueTask 做優化。
  2. 不要「隨便」用 ValueTask——會增加程式複雜度與錯誤機率。
  3. 不要對同一個 ValueTask 做多次 await。需要多次等待就轉成 Task。
  4. 與 Task 的相容性通常透過 .AsTask()。
  5. 記住 ValueTask 是個 struct,複製時行為會不同,使用時要小心。

6. 使用 ValueTask 的常見錯誤

錯誤 №1:對同一個 ValueTask 實例重複 await。 ValueTask 是 struct,不能被 await 兩次。第二次 await 會導致例外或行為不正確。

錯誤 №2:在不了解相容性下使用 ValueTask。 有些第三方 API 需要 Task,直接傳遞 ValueTask 會出問題。

錯誤 №3:不必要地使用 ValueTask。 如果結果總是非同步的,使用 ValueTask 只會讓程式更複雜但沒有實際收益。

錯誤 №4:複製 ValueTask 結構。 被複製的 struct 在 await 時可能表現異常,要盡量操作原始實例。

2
任務
C# SELF, 等級 62, 課堂 2
上鎖
立即或非同步返回結果
立即或非同步返回結果
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION