【C# Task】 ValueTask/Task


概要

1、如果異步方法的使用者使用 Task.WhenAll 或 Task.WhenAny,則在異步方法中使用 ValueTask<T> 作為返回類型可能會產生高昂的成本。這是因為您需要使用 AsTask 方法將 ValueTask<T> 轉換為 Task<T>這將產生一個分配,如果首先使用了緩存的 Task<T>,則可以輕松避免這種分配

2、每個值任務只能使用一次。此處的"消費"一詞意味着 ValueTask 可以異步等待(等待)操作完成,或者利用 AsTask 將 ValueTask 轉換為任務。但是,一個值任務只應使用一次,之后應忽略值任務<T>。

如何在 C 中使用 ValueTask#

利用 C# 中的 ValueTask,避免在從異步方法返回任務對象時進行分配

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

C# 中異步方法的建議返回類型是 Task。如果要編寫返回值的異步方法,則應返回 Task<T>。如果要編寫事件處理程序,可以改為返回 void。在 C# 7.0 之前,異步方法可以返回 Task、Task<T> 或 void。從 C# 7.0 開始,異步方法還可以返回 ValueTask(作為 System.Threading.Tasks.Extensions 包的一部分提供)或 ValueTask<T>。本文討論了如何在 C# 中使用 ValueTask。

為什么我應該使用 ValueTask?

任務表示某個操作的狀態,即操作是否已完成、是否取消等。異步方法可以返回 Task 或 ValueTask。

現在,由於 Task 是引用類型,因此從異步方法返回 Task 對象意味着每次調用該方法時都要在托管堆上分配該對象。因此,使用 Task 時需要注意的一點是,每次從方法返回 Task 對象時,都需要在托管堆中分配內存。如果方法正在執行的操作的結果立即可用或同步完成,則不需要此分配,因此成本會變得高昂。

 

這正是ValueTask的救星。ValueTask<T>提供了兩個主要優點。首先,ValueTask<T>提高了性能,因為它不需要堆分配,其次,它既簡單又靈活地實現。當結果立即可用時,通過從異步方法返回 ValueTask<T> 而不是 Task<T>,可以避免不必要的分配開銷,因為此處的"T"表示結構,而 C# 中的結構是值類型(與 Task<T> 中的"T"相反,后者表示類)。

Task 和 ValueTask 表示 C# 中的兩種主要的"可等待"類型。請注意,您無法阻止值任務。如果需要阻止,則應使用 AsTask 方法將 ValueTask 轉換為任務,然后阻止該引用 Task 對象。

另請注意,每個值任務只能使用一次。此處的"消費"一詞意味着 ValueTask 可以異步等待(等待)操作完成,或者利用 AsTask 將 ValueTask 轉換為任務。但是,一個值任務只應使用一次,之后應忽略值任務<T>。

C# 中的值任務示例

假設您有一個返回 Task 的異步方法。您可以利用 Task.FromResult 創建 Task 對象,如下面給出的代碼片段所示。

public Task<int> GetCustomerIdAsync()
{
    return Task.FromResult(1);
}
上面的代碼片段不會創建整個異步狀態機魔術,但它會在托管堆中分配一個 Task 對象。若要避免此分配,您可能希望改為利用 ValueTask,如下面給出的代碼段所示。
public ValueTask<int> GetCustomerIdAsync()
{
    return new ValueTask(1);
}

以下代碼段闡釋了 ValueTask 的同步實現。

 public interface IRepository<T>
    {
        ValueTask<T> GetData();
    }

存儲庫類擴展 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();
        }

現在,讓我們向存儲庫中添加另一個方法,這次是名為 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 意味着處理更多數據,因為方法調用將返回兩個數據字段而不是一個字段。此外,如果等待返回 ValueTask 的方法,則該異步方法的狀態機也會更大,因為在 Task 的情況下,它必須容納包含兩個字段的結構,而不是單個引用。

此外,如果異步方法的使用者使用 Task.WhenAll 或 Task.WhenAny,則在異步方法中使用 ValueTask<T> 作為返回類型可能會產生高昂的成本。這是因為您需要使用 AsTask 方法將 ValueTask<T> 轉換為 Task<T>這將產生一個分配,如果首先使用了緩存的 Task<T>,則可以輕松避免這種分配。

這是經驗法則。當您有一段始終是異步的代碼時,即當操作不會立即完成時,請使用 Task。當異步操作的結果已經可用或已有緩存的結果時,請利用 ValueTask。無論哪種方式,您都應該在考慮 ValueTask 之前執行必要的性能分析。

 


免責聲明!

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



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