最近 NCC 群裡在討論
ValueTask
/ValueTask<TResult>
,大帥(Natasha主要開發者)最近執著於搞演算法和高效能運算,他這麼關注這個東西,說明有搞頭,揹著他偷偷學一下,免得沒話題?。
ValueTask
/ValueTask<TResult>
出現時間其實比較早的了,之前一直沒有深入,藉此機會好好學習一番。
文章中說 ValueTask 時,為了減少文字數量,一般包括其泛型版本 ValueTask<TRsult>
;提到 Task,也包括其泛型版本;
1,可用版本與參考資料
根據 Microsoft 官網的參考資料,以下版本的 .NET 程式(集)可以使用 ValueTask/ValueTask<TResult>
。
版本類別 | 版本要求 |
---|---|
.NET | 5.0 |
.NET Core | 2.1、3.0、3.1 |
.NET Standard | 2.1 |
以下是筆者閱讀時的參考資料連結地址:
【1】 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.valuetask?view=net-5.0
【2]】 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.valuetask-1?view=net-5.0
【3】 https://www.infoworld.com/article/3565433/how-to-use-valuetask-in-csharp.html
【4】 https://tooslowexception.com/implementing-custom-ivaluetasksource-async-without-allocations/
【5】 https://blog.marcgravell.com/2019/08/prefer-valuetask-to-task-always-and.html
【6】 https://qiita.com/skitoy4321/items/31a97e03665bd7bcc8ca
【7】 https://neuecc.medium.com/valuetasksupplement-an-extensions-to-valuetask-4c247bc613ea
2,ValueTask<TResult>
和 Task
ValueTask<TResult>
存在於 System.Threading.Tasks
名稱空間下,ValueTask<TResult>
的定義如下:
public struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>
而 Task 的定義如下:
public class Task : IAsyncResult, IDisposable
從其繼承的介面和官方文件來看,ValueTask<TResult>
複雜度應該不高。
根據文件表面理解,這個型別,應該是 Task 的簡化版本,Task 是引用型別,因此從非同步方法返回 Task 物件或者每次呼叫非同步方法時,都會在託管堆中分配該物件。
根據比較,我們應當知道:
- Task 是引用型別,會在託管堆中分配記憶體;ValueTask 是值型別;
目前就只有這一點需要記住,下面我們繼續比較兩者的異同點。
這裡我們嘗試一下使用這個型別對比 Task ,看看程式碼如何。
public static async ValueTask<int> GetValueTaskAsync()
{
await Task.CompletedTask; // 這裡別誤會,這是隨便找個地方 await 一下
return 666;
}
public static async Task<int> GetTaskAsync()
{
await Task.CompletedTask;
return 666;
}
從程式碼上看,兩者在簡單程式碼上使用的方法一致(CURD基本就是這樣)。
3,編譯器如何編譯
Task 在編譯時,由編譯器生成狀態機,會為每個方法生成一個繼承 IAsyncStateMachine
的類,並且出現大量的程式碼包裝。
據筆者測試,ValueTask 也是生成類似的程式碼。
如圖:
訪問 https://sharplab.io/#gist:ddf2a5e535a34883733196c7bf4c55b2 可線上閱讀以上程式碼(Task)。
訪問 https://sharplab.io/#gist:7129478fc630a87c08ced38e7fd14cc0 線上閱讀 ValueTask 示例程式碼。
你分別訪問這裡 URL,對比差異。
筆者將有差異的部分取出來了,讀者可以認真看一下:
Task:
[AsyncStateMachine(typeof(<GetTaskAsync>d__0))]
[DebuggerStepThrough]
public static Task<int> GetTaskAsync()
{
<GetTaskAsync>d__0 stateMachine = new <GetTaskAsync>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
ValueTask:
[AsyncStateMachine(typeof(<GetValueTaskAsync>d__0))]
[DebuggerStepThrough]
public static ValueTask<int> GetValueTaskAsync()
{
<GetValueTaskAsync>d__0 stateMachine = new <GetValueTaskAsync>d__0();
stateMachine.<>t__builder = AsyncValueTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
AsyncValueTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
我是沒看出有啥區別。。。
不過這裡要提到第二點:
- 如果這個方法的處理速度很快,或者你的程式碼執行後立即可用等,使用非同步並不會比同步快,反而有可能多消耗一下效能資源。
4,ValueTask 有什麼優勢
從前面的內容可知,ValueTask 跟 Task 編譯後生成的狀態機程式碼一致,那麼真正有區別的地方,就是 ValueTask 是值型別,Task 是引用型別。
從功能上看,ValueTask 是簡單的非同步表示,而 Task 具有很多強大的方法,有各種各樣的騷操作。
ValueTask 因為不需要堆分配記憶體而提高了效能,這是 ValueTask 對 Task 有優勢的地方。
要避免記憶體分配開銷,我們可以使用 ValueTask 包裝需要返回的結果。
public static ValueTask<int> GetValueTask()
{
return new ValueTask<int>(666);
}
public static async ValueTask<int> StartAsync()
{
return await GetValueTask();
}
但是目前,我們還沒有進行任何效能測試,不足以說明 ValueTask 對提高效能的優勢,筆者繼續講解一些基礎知識,待時機成熟後,會進行一些測試並放出示例程式碼。
5,ValueTask 建立非同步任務
我們看一下 ValueTask
和 ValueTask<TResult>
的建構函式定義。
// ValueTask
public ValueTask(Task task);
public ValueTask(IValueTaskSource source, short token);
// ValueTask<TResult>
public ValueTask(Task<TResult> task);
public ValueTask(TResult result);
public ValueTask(IValueTaskSource<TResult> source, short token);
如果通過 Task 建立任務,可以使用 new Task()
、Task.Run()
等方式建立一個任務,然後就可以使用 async/await
關鍵字 定義非同步方法,開啟非同步任務。那麼如果使用 ValueTask 呢?
第四小節我們已經有了示例,使用了 ValueTask(TResult result)
建構函式,可以自己 new ValueTask
,然後就可以使用 await
關鍵字。
另外, ValueTask 的建構函式有多個,我們可以繼續挖掘一下。
通過 Task 轉換為 ValueTask:
public static async ValueTask<int> StartAsync()
{
Task<int> task = Task.Run<int>(() => 666);
return await new ValueTask<int>(task);
}
剩下一個 IValueTaskSource
引數型別做建構函式的方法,我們放到第 6 小節講。
6,IValueTaskSource 和自定義包裝 ValueTask
關於 IValueTaskSource
IValueTaskSource 在 System.Threading.Tasks.Sources
名稱空間中,其定義如下:
public interface IValueTaskSource
{
void GetResult(short token);
ValueTaskSourceStatus GetStatus(short token);
void OnCompleted(
Action<object?> continuation,
object? state,
short token,
ValueTaskSourceOnCompletedFlags flags);
}
方法名稱 | 作用 |
---|---|
GetResult(Int16) | 獲取 IValueTaskSource 的結果,僅在非同步狀態機需要獲取操作結果時呼叫一次 |
GetStatus(Int16) | 獲取當前操作的狀態,由非同步狀態機呼叫以檢查操作狀態 |
OnCompleted(Action, Object, Int16, ValueTaskSourceOnCompletedFlags) | 為此 IValueTaskSource 計劃延續操作,開發者自己呼叫 |
在這個名稱空間中,還有一些跟 ValueTask 相關的型別,可參考 微軟文件。
在上述三個方法中,OnCompleted
用於延續任務,這個方法熟悉 Task 的讀者應該都清楚,這裡就不再贅述。
前面我們有一個示例:
public static ValueTask<int> GetValueTask()
{
return new ValueTask<int>(666);
}
public static async ValueTask<int> StartAsync()
{
return await GetValueTask();
}
編譯器轉換後的簡化程式碼:
public static int _StartAsync()
{
var awaiter = GetValueTask().GetAwaiter();
if (!awaiter.IsCompleted)
{
// 一些莫名其妙的操作程式碼
}
return awaiter.GetResult();
}
基於這個程式碼,我們發現 ValueTask 可以有狀態感知,那麼如何表達任務已經完成?裡面又有啥實現原理?
什麼是 IValueTaskSource
IValueTaskSource 是一種抽象,通過這種抽象我們可以將 任務/操作 的邏輯行為和結果本身分開表示(狀態機)。
簡化示例:
IValueTaskSource<int> someSource = // ...
short token = // ...令牌
var vt = new ValueTask<int>(someSource, token); // 建立任務
int value = await vt; // 等待任務完成
但從這段程式碼來看,我們無法看到 如何實現 IValueTaskSource,ValueTask 內部又是如何使用 IValueTaskSource 的。在深入其原理之前,筆者從其它部落格、文件等地方查閱到,為了降低 Task(C#5.0引入) 的效能開銷,C# 7.0 出現了 ValueTask。ValueTask 的出現是為了包裝返回結果,避免使用堆分配。
所以,需要使用 Task 轉換為 ValueTask:
public ValueTask(Task task); // ValueTask 建構函式
ValueTask 只是包裝 Task 的返回結果。
後來,為了更高的效能,引入了 IValueTaskCource,ValueTask 便多增加了一個建構函式。
可以通過實現 IValueTaskSource:
public ValueTask(IValueTaskSource source, short token); // ValueTask 建構函式
這樣,可以進一步消除 ValueTask 跟 Task 轉換的效能開銷。ValueTask 便擁有狀態“管理”能力,不再依賴 Task 。
再說 ValueTask 優勢
2019-8-22 的 coreclr 草案中,有個主題 “Make "async ValueTask/ValueTask
Issue 地址:https://github.com/dotnet/coreclr/pull/26310
裡面有各種各樣的效能指標比較,筆者十分推薦有興趣深入研究的讀者看一下這個 Issue。
不要自己全部實現 IValueTaskSource
大多數人無法完成這個介面,我個人看來很多次也沒有看懂,翻了很久,沒有找到合適的程式碼示例。根據官方的文件,我發現了 ManualResetValueTaskSourceCore
,這個型別實現了 IValueTaskSource
介面,並且進行了封裝,因此我們可以使用 ManualResetValueTaskSourceCore
對自己的程式碼進行包裝,更加輕鬆地實現 IValueTaskSource。
關於 ManualResetValueTaskSourceCore
,文章後面再給出使用方法和程式碼示例。
ValueTaskSourceOnCompletedFlags
ValueTaskSourceOnCompletedFlags 是一個列舉,用於表示延續的行為,其列舉說明如下:
列舉 | 值 | 說明 |
---|---|---|
FlowExecutionContext | 2 | OnCompleted 應捕獲當前 ExecutionContext 並用它來執行延續。 |
None | 0 | 對延續的呼叫方式內有任何要求。 |
UseSchedulingContext | 1 | OnCompleted 應該捕獲當前排程上下文(SynchronizationContext),並在將延續加入執行佇列時使用。 如果未設定此標誌,實現可以選擇執行任意位置的延續。 |
ValueTaskSourceStatus
ValueTaskSourceStatus 列舉用於指示 指示 IValueTaskSource 或 IValueTaskSource 的狀態,其列舉說明如下:
列舉 | 值 | 說明 |
---|---|---|
Canceled | 3 | 操作因取消操作而完成。 |
Faulted | 2 | 操作已完成但有錯誤。 |
Pending | 0 | 操作尚未完成。 |
Succeeded | 1 | 操作已成功完成。 |
7,編寫 IValueTaskSource 例項
完整程式碼:https://github.com/whuanle/RedisClientLearn/issues/1
假如我們要設計一個 Redis 客戶端,並且實現非同步,如果你有 Socket 開發經驗,會了解 Socket 並不是 一發一收的。C# 中的 Socket 中也沒有直接的非同步介面。
所以這裡我們要實現一個非同步的 Redis 客戶端。
使用 IValueTaskSource 編寫狀態機:
// 一個可以將同步任務、不同執行緒同步操作,通過狀態機構建非同步方法
public class MyValueTaskSource<TRusult> : IValueTaskSource<TRusult>
{
// 儲存返回結果
private TRusult _result;
private ValueTaskSourceStatus status = ValueTaskSourceStatus.Pending;
// 此任務有異常
private Exception exception;
#region 實現介面,告訴呼叫者,任務是否已經完成,以及是否有結果,是否有異常等
// 獲取結果
public TRusult GetResult(short token)
{
// 如果此任務有異常,那麼獲取結果時,重新彈出
if (status == ValueTaskSourceStatus.Faulted)
throw exception;
// 如果任務被取消,也彈出一個異常
else if (status == ValueTaskSourceStatus.Canceled)
throw new TaskCanceledException("此任務已經被取消");
return _result;
}
// 獲取狀態,這個示例中,用不到令牌 token
public ValueTaskSourceStatus GetStatus(short token)
{
return status;
}
// 實現延續
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
{
// 不需要延續,不實現此介面
}
#endregion
#region 實現狀態機,能夠控制此任務是否已經完成,以及是否有異常
// 以及完成任務,並給出結果
public void SetResult(TRusult result)
{
status = ValueTaskSourceStatus.Succeeded; // 此任務已經完成
_result = result;
}
// 取消任務
public void Cancel()
{
status = ValueTaskSourceStatus.Canceled;
}
// 要執行的任務出現異常
public void SetException(Exception exception)
{
this.exception = exception;
status = ValueTaskSourceStatus.Faulted;
}
#endregion
}
假的 Socket:
public class 假的Socket
{
private bool IsHaveSend = false;
// 模擬 Socket 向伺服器傳送資料
public void Send(byte[] data)
{
new Thread(() =>
{
Thread.Sleep(100);
IsHaveSend = true;
}).Start();
}
// 同步阻塞等待伺服器的響應
public byte[] Receive()
{
// 模擬網路傳輸的資料
byte[] data = new byte[100];
while (!IsHaveSend)
{
// 伺服器沒有傳送資料到客戶端時,一直空等待
}
// 模擬網路接收資料耗時
Thread.Sleep(new Random().Next(0, 100));
new Random().NextBytes(data);
IsHaveSend = false;
return data;
}
}
實現 Redis 客戶端,並且實現
// Redis 客戶端
public class RedisClient
{
// 佇列
private readonly Queue<MyValueTaskSource<string>> queue = new Queue<MyValueTaskSource<string>>();
private readonly 假的Socket _socket = new 假的Socket(); // 一個 socket 客戶端
public RedisClient(string connectStr)
{
new Thread(() =>
{
while (true)
{
byte[] data = _socket.Receive();
// 從佇列中拿出一個狀態機
if (queue.TryDequeue(out MyValueTaskSource<string> source))
{
// 設定此狀態機的結果
source.SetResult(Encoding.UTF8.GetString(data));
}
}
}).Start();
}
private void SendCommand(string command)
{
Console.WriteLine("客戶端傳送了一個命令:" + command);
_socket.Send(Encoding.UTF8.GetBytes(command));
}
public async ValueTask<string> GetStringAsync(string key)
{
// 自定義狀態機
MyValueTaskSource<string> source = new MyValueTaskSource<string>();
// 建立非同步任務
ValueTask<string> task = new ValueTask<string>(source, 0);
// 加入佇列中
queue.Enqueue(source);
// 傳送獲取值的命令
SendCommand($"GET {key}");
// 直接使用 await ,只會檢查移除狀態!一層必須在檢查之前完成任務,然後 await 後會陷入無限等待中!
// return await task;
// 要想真正實現這種非同步,必須使用 SynchronizationContext 等複雜的結構邏輯!
// 為了避免過多程式碼,我們可以使用下面這種 無限 while 的方法!
var awaiter = task.GetAwaiter();
while (!awaiter.IsCompleted) { }
// 返回結果
return await task;
}
}
大概思路就是這樣。但是最後是無法像 Task 那樣直接 await 的!ValueTask 只能 await 一次,並且 await 只能是最後的結果檢查!
如果我們使用 TaskCompletionSource
寫 Task 狀態機,是可以直接 await 的。
如果你要真正實現可以 await 的 ValueTask,那麼編寫 IValueTasksource
時,必須實現 SynchronizationContext
、TaskScheduler
等。
實現這些程式碼,比較複雜,怎麼辦?微軟官方給出了一個ManualResetValueTaskSourceCore<TResult>
,有了它,我們可以省去很多複雜的程式碼!
8,使用 ManualResetValueTaskSourceCore
接下來,我們通過 ManualResetValueTaskSourceCore
改造以往的程式碼,這樣我們可以直觀的感受到這個型別是用來幹嘛的!
改造 MyValueTaskSource
如下:
// 一個可以將同步任務、不同執行緒同步操作,通過狀態機構建非同步方法
public class MyValueTaskSource<TRusult> : IValueTaskSource<TRusult>
{
private ManualResetValueTaskSourceCore<TRusult> _source = new ManualResetValueTaskSourceCore<TRusult>();
#region 實現介面,告訴呼叫者,任務是否已經完成,以及是否有結果,是否有異常等
// 獲取結果
public TRusult GetResult(short token)
{
return _source.GetResult(token);
}
// 獲取狀態,這個示例中,用不到令牌 token
public ValueTaskSourceStatus GetStatus(short token)
{
return _source.GetStatus(token); ;
}
// 實現延續
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
{
_source.OnCompleted(continuation, state, token, flags);
}
#endregion
#region 實現狀態機,能夠控制此任務是否已經完成,以及是否有異常
// 以及完成任務,並給出結果
public void SetResult(TRusult result)
{
_source.SetResult(result);
}
// 要執行的任務出現異常
public void SetException(Exception exception)
{
_source.SetException(exception);
}
#endregion
}
之後,我們可以直接在 GetStringAsync
使用 await 了!
public async ValueTask<string> GetStringAsync(string key)
{
// 自定義狀態機
MyValueTaskSource<string> source = new MyValueTaskSource<string>();
// 建立非同步任務
ValueTask<string> task = new ValueTask<string>(source, 0);
// 加入佇列中
queue.Enqueue(source);
// 傳送獲取值的命令
SendCommand($"GET {key}");
return await task;
}
到此為止,ValueTask、IValueTaskSource、ManualResetValueTaskSourceCore,你搞明白了沒有!
有人給 ValueTask 實現了大量擴充,使得 ValueTask 擁有跟 Task 一樣多工併發能力,例如 WhenAll、WhenAny、Factory等,擴充庫地址:https://github.com/Cysharp/ValueTaskSupplement
時間原因(筆者一般11點就睡),本文筆者就不給出併發以及其它情況下的 GC 和效能比較了,大家學會使用後,可以自行測試。
可關注 NCC 公眾號,瞭解更多效能知識!