一:背景
1. 講故事
最近聊了不少和異步相關的話題,有點疲倦了,今天再寫最后一篇作為近期這類話題的一個封筆吧,下篇繼續寫我熟悉的 生產故障
系列,突然親切感油然而生,哈哈,免費給別人看程序故障,是一種積陰德陽善的事情,欲知前世因,今生受者是。欲知來世果,今生做者是。
在任務延續方面,我個人的總結就是三類,分別為:
- StateMachine
- ContinueWith
- Awaiter
話不多說,我們逐個研究下底層是咋玩的?
二:異步任務延續的玩法
1. StateMachine
說到狀態機大家再熟悉不過了,也是 async,await 的底層化身,很多人看到 async await 就想到了IO場景,其實IO場景和狀態機是兩個獨立的東西,狀態機是一種設計模式,把這個模式套在IO場景會讓代碼更加絲滑,僅此而已。為了方便講述,我們寫一個 StateMachine 與 IO場景 無關的一段測試代碼。
internal class Program
{
static void Main(string[] args)
{
UseAwaitAsync();
Console.ReadLine();
}
static async Task<string> UseAwaitAsync()
{
var html = await Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客園</h1></html>";
return response;
});
Console.WriteLine($"GetStringAsync 的結果:{html}");
return html;
}
}
那這段代碼在底層是如何運作的呢?剛才也說到了asyncawait只是迷惑你的一種幻象,我們必須手握辟邪寶劍
斬開幻象顯真身,這里借助 ilspy 截圖如下:
從卦中看,本質上就是借助AsyncTaskMethodBuilder<string>
建造者將 awaiter 和 stateMachine 做了一個綁定,感興趣的朋友可以追一下 AwaitUnsafeOnCompleted() 方法,最后狀態機 <UseAwaitAsync>d__1
實例會放入到 Task.Run
的 m_continuationObject 字段。如果有朋友對流程比較蒙的話,我畫了一張簡圖。
圖和代碼都有了,接下來就是眼見為實。分別在 AddTaskContinuation
和 RunContinuations
方法中做好埋點,前者可以看到 延續任務 是怎么加進去的,后者可以看到 延續任務 是怎么取出來的。
心細的朋友會發現這卦上有一個很特別的地方,就是 allowInlining=true
,也就是回調函數(StateMachine)是在當前線程上一擼到底的。
有些朋友可能要問,能不能讓延續任務
跑在單獨線程上? 可以是可以,但你得把 Task.Run 改成 Task.Factory.StartNew ,這樣就可以設置TaskCreationOptions參數,參考代碼如下:
var html = await Task.Factory.StartNew(() =>{}, TaskCreationOptions.RunContinuationsAsynchronously);
2. ContinueWith
那些同處于被裁的35歲大齡程序員應該知道Task是 framework 4.0 時代出來的,而async,await是4.5出來的,所以在這個過渡期中有大量的項目會使用ContinueWith 導致回調地獄。。。 這里我們對比一下兩者有何不同,先寫一段參考代碼。
internal class Program
{
static void Main(string[] args)
{
UseContinueWith();
Console.ReadLine();
}
static Task<string> UseContinueWith()
{
var query = Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客園</h1></html>";
return response;
}).ContinueWith(t =>
{
var html = t.Result;
Console.WriteLine($"GetStringAsync 的結果:{html}");
return html;
});
return query;
}
}
從卦代碼看確實沒有asyncawait簡潔,那 ContinueWith 內部做了什么呢?感興趣的朋友可以跟蹤一下,本質上和 StateMachine 的玩法是一樣的,都是借助 m_continuationObject 來實現延續,畫個簡圖如下:
代碼和模型圖都有了,接下來就是用 dnspy 開干了。。。還是在 AddTaskContinuation
和 RunContinuations
上埋伏斷點觀察。
從卦中可以看到,延續任務使用新線程來執行的,并沒有一擼到底,這明顯與 asyncawait
的方式不同,有些朋友可能又要說了,那如何實現和StateMachine一樣的呢?這就需要在 ContinueWith 中新增 ExecuteSynchronously 同步參數,參考如下:
var query = Task.Run(() => { }).ContinueWith(t =>
{
}, TaskContinuationOptions.ExecuteSynchronously);
3. Awaiter
使用Awaiter做任務延續的朋友可能相對少一點,它更多的是和 StateMachine 打配合,當然單獨使用也可以,但沒有前兩者靈活,它更適合那些不帶返回值的任務延續,本質上也是借助 m_continuationObject
字段實現的一套底層玩法,話不多說,上一段代碼:
static Task<string> UseAwaiter()
{
var awaiter = Task.Run(() =>
{
Thread.Sleep(1000);
var response = "<html><h1>博客園</h1></html>";
return response;
}).GetAwaiter();
awaiter.OnCompleted(() =>
{
var html = awaiter.GetResult();
Console.WriteLine($"UseAwaiter 的結果:{html}");
});
return Task.FromResult(string.Empty);
}
前面兩種我配了圖,這里沒有理由不配了,哈哈,模型圖如下:
接下來把程序運行起來,觀察截圖:
從卦中觀察,它和StateMachine一樣,默認都是 一擼到底 的方式。
三:RunContinuations 觀察
這一小節我們單獨說一下 RunContinuations
方法,因為這里的實現太精妙了,不幸的是Dnspy和ILSpy反編譯出來的代碼太狗血,原汁原味的簡化后代碼如下:
private void RunContinuations(object continuationObject)
{
bool canInlineContinuations =
(m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
RuntimeHelpers.TryEnsureSufficientExecutionStack();
switch (continuationObject)
{
case IAsyncStateMachineBox stateMachineBox:
AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
LogFinishCompletionNotification();
return;
case Action action:
AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
LogFinishCompletionNotification();
return;
case TaskContinuation tc:
tc.Run(this, canInlineContinuations);
LogFinishCompletionNotification();
return;
case ITaskCompletionAction completionAction:
RunOrQueueCompletionAction(completionAction, canInlineContinuations);
LogFinishCompletionNotification();
return;
}
}
卦中的 case 挺有意思的,除了本篇聊過的 TaskContinuation 和 IAsyncStateMachineBox 之外,還有另外兩種 continuationObject,這里說一下 ITaskCompletionAction 是怎么回事,其實它是 Task.Result
的底層延續類型,所以大家應該能理解為什么 Task.Result 能喚醒,主要是得益于Task.m_continuationObject =completionAction
所致。
說了這么說,如何眼見為實呢?可以從源碼中尋找答案。
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
var mres = new SetOnInvokeMres();
AddCompletionAction(mres, addBeforeOthers: true);
var returnValue = mres.Wait(Timeout.Infinite, cancellationToken);
}
private sealed class SetOnInvokeMres : ManualResetEventSlim, ITaskCompletionAction
{
internal SetOnInvokeMres() : base(false, 0) { }
public void Invoke(Task completingTask) { Set(); }
public bool InvokeMayRunArbitraryCode => false;
}
從卦中可以看到,其實就是把 ITaskCompletionAction 接口的實現類 SetOnInvokeMres 塞入了 Task.m_continuationObject 中,一旦Task執行完畢之后就會調用 Invoke() 下的 Set()
來實現事件喚醒。
四:總結
雖然異步任務延續
有三種實現方法,但底層都是一個套路,即借助 Task.m_continuationObject
字段玩出的各種花樣,當然他們也是有一些區別的,即對 m_continuationObject
任務是否用單獨的線程調度,產生了不同的意見分歧。
?轉自https://www.cnblogs.com/huangxincheng/p/18662162
該文章在 2025/1/10 8:50:58 編輯過