1. 程式人生 > 其它 >如何在 C# 中使用 ValueTask

如何在 C# 中使用 ValueTask

非同步程式設計 相信大家已經使用很多年了,尤其在 C# 5.0 中引入的 await,async 關鍵詞讓程式碼編寫的更加簡單粗暴,你可以利用 非同步程式設計 來提高應用程式的 響應速度吞吐率

非同步方法中推薦的做法是返回 Task,如果非同步方法中需要返回一些資料的話,可以將 Task 改成 Task<T>,還有一種情況是你的非同步方法什麼都不需要返回,那就改成 void 就可以了。

在 C# 7.0 之前,非同步方法的返回值有以下三種。

  • Task

  • Task<T>

  • void

在 C# 7.0 之後,除了上面三種還可以返回 ValueTaskValueTask<T>

,這些類都是在 System.Threading.Tasks.Extensions 名稱空間下,這篇文章我準備和大家一起討論下 ValueTask。

為什麼要使用 ValueTask

Task 可用來表示操作的狀態,什麼意思呢?比如說:這個操作是否完成?是否被取消 等等,同時 非同步方法 中可以返回 Task 或者 ValueTask,這裡有個潛在的問題不知道大家是否注意到,因為Task 是一個引用型別,如下程式碼:


    public class Task : IAsyncResult, IDisposable
    {
    }

這就意味著每次呼叫非同步方法都會在 託管堆 上生成一個 Task 例項,如果瞬間呼叫 1w 次,那麼託管堆上也瞬間存在 1w 個 Task 例項,那這有什麼問題呢? 問題在於有些場景下,你的非同步方法是可以直接返回資料的,或者說可以完全同步的,比如:你的非同步方法僅僅是從快取中取資料,這時候無謂的給 GC 增加回收壓力就不那麼優美了。


        static Task<int> GetCustomerCountAsyc(string key)
        {
            return Task.FromResult<int>(NativeCache[key]);
        }

這時候就需要用 ValueTask 了,它提供瞭如下兩點好處:

  • ValueTask 是值型別,所以避免了在 託管堆 上的記憶體分配,便擁有了更高的效能。

  • ValueTask 在實現上更加便捷 而且 靈活性更強。

總的來說: 如果你的非同步方法可以直接獲取結果,那麼建議將 Task<T>

改成 ValueTask<T>,從而避免不必要的效能開銷,TaskValueTask 都是表示 可等待 的操作,這裡要注意的是,千萬不要阻塞 ValueTask,畢竟它是值型別,如果真要這麼做的話,呼叫 ValueTask.AsTask 方法將 ValueTask 轉成 Task,然後在這個引用的 Task 上進行阻塞,還有一點要注意的是,ValueTask 只能被 await 一次,如果要破除這個限制的話,還是呼叫 AsTask 方法 將 ValueTask 轉成 Task 即可。

ValueTask 的例子

假設你有一個非同步方法,需要返回 Task,你可以利用 Task.FromResult 去生成一個 Task 物件,如下程式碼所示:


public Task<int> GetCustomerIdAsync()
{
    return Task.FromResult(1);
}

上面的程式碼不會在 IL 層面生成完整的 狀態機程式碼,而僅僅是在 託管堆 中生成一個 Task 物件,要避免這種無謂的Task分配,可以用 ValueTask 擼掉它,如下程式碼所示:


public ValueTask<int> GetCustomerIdAsync()
{
    return new ValueTask(1);
}

接下來定義一個 IRepository 介面,新增一個返回值為 ValueTask<int> 的同步方法,如下程式碼所示:


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

從 IRepository 介面上派生一個 Repository 類,如下程式碼所示:


    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();
        }

現在在 IRepository 介面中新增一個非同步方法 GetDataAsync,修改後的程式碼如下:


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

    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;
        }
    }

什麼時候應該使用 ValueTask

儘管 ValueTask 提供了很多好處,但用 ValueTask 替換掉 Task 也必須再三權衡,因為 ValueTask 是一個包含兩個欄位的值型別,而 Task 僅僅是一個欄位的引用型別,這就意味著從方法返回 ValueTask 會有兩個欄位的開銷,同時在 await 場景下生成的 非同步狀態機 需要更大的空間來儲存 兩個欄位的 ValueTask 型別。

再擴充套件的話,如果非同步方法的呼叫者使用 Task.WhenAll 或者 Task.WhenAny 時,而這個非同步方法返回值是 ValueTask 的話開銷通常會更大,為什麼這麼說呢? 因為你必須要將 ValueTask 轉成 Task 才能在 WhenXXX 中使用,這個過程中就造成了託管堆的記憶體分配,如果想優化的話,可以在第一次使用 Task 的時候將這個例項快取起來,供後續再複用。

最後總結一些 經驗法則 吧。

  • 如果你的非同步方法不是可立即完成的,請用 Task。

  • 如果你的非同步方法是可立即完成的,比如純記憶體操作,讀快取,請用 ValueTask。

不管怎樣,在使用 ValueTask 之前一定要做好必要的效能分析,給自己充足的理由使用 ValueTask。

譯文連結:https://www.infoworld.com/article/3565433/how-to-use-valuetask-in-csharp.html

更多高質量乾貨:參見我的 GitHub: csharptranslate