如何使用 C# 中的 ValueTask


在 C# 中利用 ValueTask 避免從異步方法返回 Task 對象時分配

翻譯自 Joydip Kanjilal 2020年7月6日 的文章 《How to use ValueTask in C#》

異步編程已經使用了相當長一段時間了。近年來,隨着 asyncawait 關鍵字的引入,它變得更加強大。您可以利用異步編程來提高應用程序的響應能力和吞吐量。

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 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# 中 TaskValueTask 表示兩種主要的 “可等待(awaitable)” 類型。請注意,您不能阻塞(block)一個 ValueTask。如果需要阻塞,則應使用 AsTask 方法將 ValueTask 轉換為 Task,然后在該引用 Task 對象上進行阻塞。

另外請注意,每個 ValueTask 只能被消費(consumed)一次。這里的單詞 “消費(consume)” 是指 ValueTask 可以異步等待(await)操作完成,或者利用 AsTaskValueTask 轉換為 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 之前,您都應該執行必要的性能分析。


ValueTaskreadonly struct 類型,Taskclass 類型。
相關鏈接:C# 中 Struct 和 Class 的區別總結


作者 : Joydip Kanjilal
譯者 : 技術譯民
出品 : 技術譯站
鏈接 : 英文原文


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM