C#中async/await之線程上下文工作原理
引言
接《async/await 在 C# 語言中是如何工作的?(上)》、《async/await 在 C# 語言中是如何工作的?(中)》,今天我們繼續(xù)介紹 SynchronizationContext 和 ConfigureAwait。
SynchronizationContext 和 ConfigureAwait
我們之前在 EAP 模式的上下文中討論過 SynchronizationContext,并提到它將再次出現(xiàn)。SynchronizationContext 使得調(diào)用可重用的輔助函數(shù)成為可能,并自動(dòng)被調(diào)度回調(diào)用環(huán)境認(rèn)為合適的任何地方。因此,我們很自然地認(rèn)為 async/await 能“正常工作”,事實(shí)也的確如此?;氐角懊娴陌粹o單擊處理程序:
ThreadPool.QueueUserWorkItem(_ => { string message = ComputeMessage(); button1.BeginInvoke(() => { button1.Text = message; }); });
使用 async/await,我們可以這樣寫:
button1.Text = await Task.Run(() => ComputeMessage());
對 ComputeMessage 的調(diào)用被轉(zhuǎn)移到線程池中,這個(gè)方法執(zhí)行完畢后,執(zhí)行又轉(zhuǎn)移回與按鈕關(guān)聯(lián)的 UI 線程,設(shè)置按鈕的 Text 屬性就是在這個(gè)線程中進(jìn)行的。
與 SynchronizationContext 的集成由 awaiter 實(shí)現(xiàn)(為狀態(tài)機(jī)生成的代碼對 SynchronizationContext 一無所知),因?yàn)楫?dāng)所表示的異步操作完成時(shí),是 awaiter 負(fù)責(zé)實(shí)際調(diào)用或?qū)⑺峁┑?continuation 排隊(duì)。而自定義 awaiter 不需要考慮 SynchronizationContext。目前,Task、Task<TResult>、ValueTask、ValueTask<TResult> 的等待器都是 do。這意味著,默認(rèn)情況下,當(dāng)你等待一個(gè)任務(wù),一個(gè) Task<TResult>,一個(gè) ValueTask,一個(gè) ValueTask<TResult>,甚至 Task. yield() 調(diào)用的結(jié)果時(shí),awaiter 默認(rèn)會(huì)查找當(dāng)前的 SynchronizationContext,如果它成功地獲得了一個(gè)非默認(rèn)的同步上下文,最終會(huì)將 continuation 排隊(duì)到該上下文。
如果我們查看 TaskAwaiter 中涉及的代碼,就可以看到這一點(diǎn)。以下是 Corelib 中的相關(guān)代碼片段:
internal void UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext) { if (continueOnCapturedContext) { SynchronizationContext? syncCtx = SynchronizationContext.Current; if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext)) { var tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false); if (!AddTaskContinuation(tc, addBeforeOthers: false)) { tc.Run(this, canInlineContinuationTask: false); } return; } else { TaskScheduler? scheduler = TaskScheduler.InternalCurrent; if (scheduler != null && scheduler != TaskScheduler.Default) { var tc = new TaskSchedulerAwaitTaskContinuation(scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false); if (!AddTaskContinuation(tc, addBeforeOthers: false)) { tc.Run(this, canInlineContinuationTask: false); } return; } } } ... }
這是一個(gè)方法的一部分,用于確定將哪個(gè)對象作為 continuation 存儲(chǔ)到任務(wù)中。它被傳遞給 stateMachineBox,如前所述,它可以直接存儲(chǔ)到任務(wù)的 continuation 列表中。但是,這個(gè)特殊的邏輯可能會(huì)將 IAsyncStateMachineBox 封裝起來,以合并一個(gè)調(diào)度程序(如果存在的話)。它檢查當(dāng)前是否有非默認(rèn)的 SynchronizationContext,如果有,它會(huì)創(chuàng)建一個(gè) SynchronizationContextAwaitTaskContinuation 作為實(shí)際的對象,它會(huì)被存儲(chǔ)為 continuation;
該對象依次包裝了原始的和捕獲的 SynchronizationContext,并知道如何在與后者排隊(duì)的工作項(xiàng)中調(diào)用前者的 MoveNext。這就是如何在 UI 應(yīng)用程序中作為事件處理程序的一部分等待,并在等待完成后讓代碼繼續(xù)在正確的線程上運(yùn)行。這里要注意的下一個(gè)有趣的事情是,它不僅僅關(guān)注一個(gè) SynchronizationContext:如果它找不到一個(gè)自定義的 SynchronizationContext 來使用,它還會(huì)查看 Tasks 使用的 TaskScheduler 類型是否有一個(gè)需要考慮的自定義類型。和 SynchronizationContext 一樣,如果有一個(gè)非默認(rèn)值,它就會(huì)和原始框一起包裝在 TaskSchedulerAwaitTaskContinuation 中,用作 continuation 對象。
但這里最值得注意的可能是方法主體的第一行:if (continueOnCapturedContext)。我們只在 continueOnCapturedContext 為 true 時(shí)才對 SynchronizationContext/TaskScheduler 進(jìn)行這些檢查;如果這個(gè)值為 false,實(shí)現(xiàn)方式就好像兩者都是默認(rèn)值一樣,會(huì)忽略它們。請問是什么將 continueOnCapturedContext 設(shè)置為 false?你可能已經(jīng)猜到了:使用非常流行的 ConfigureAwait(false)。
可以這樣說,作為 await 的一部分,ConfigureAwait(false) 做的唯一一件事是將它的參數(shù)布爾值作為 continueOnCapturedContext 值提供給這個(gè)函數(shù)(以及其他類似的函數(shù)),以便跳過對 SynchronizationContext/TaskScheduler 的檢查,表現(xiàn)得好像它們都不存在一樣。對于進(jìn)程來說,這允許 Task 在它認(rèn)為合適的地方調(diào)用其 continuation,而不是強(qiáng)制將它們排隊(duì)在某個(gè)特定的調(diào)度器上執(zhí)行。
我之前提到過 SynchronizationContext 的另一個(gè)方面,我說過我們會(huì)再次看到它:OperationStarted/OperationCompleted?,F(xiàn)在是時(shí)候了。這是沒那么受歡迎的特性:異步 void。除了 configureawait 之外,async void 可以說是 async/await 中最具爭議性的特性之一。它被添加的原因只有一個(gè):事件處理程序。在 UI 應(yīng)用程序中,你可以編寫如下代碼:
button1.Click += async (sender, eventArgs) => { button1.Text = await Task.Run(() => ComputeMessage()); };
但如果所有的異步方法都必須有一個(gè)像 Task 這樣的返回類型,你就不能這樣做了。Click 事件有一個(gè)簽名 public event EventHandler? Click;,其中 EventHandler 定義為 public delegate void EventHandler(object? sender, EventArgs e);,因此要提供一個(gè)符合該簽名的方法,該方法需要是 void-returning。
有各種各樣的理由認(rèn)為 async void 是不好的,為什么文章建議盡可能避免使用它,以及為什么出現(xiàn)了各種 analyzers 來標(biāo)記使用 async void。最大的問題之一是委托推理??紤]下面的程序:
using System.Diagnostics; Time(async () => { Console.WriteLine("Enter"); await Task.Delay(TimeSpan.FromSeconds(10)); Console.WriteLine("Exit"); }); static void Time(Action action) { Console.WriteLine("Timing..."); Stopwatch sw = Stopwatch.StartNew(); action(); Console.WriteLine($"...done timing: {sw.Elapsed}"); }
人們很容易期望它輸出至少10秒的運(yùn)行時(shí)間,但如果你運(yùn)行它,你會(huì)發(fā)現(xiàn)輸出是這樣的:
Timing...
Enter
...done timing: 00:00:00.0037550
async lambda 實(shí)際上是一個(gè)異步 void 方法。異步方法會(huì)在遇到第一個(gè)暫停點(diǎn)時(shí)返回調(diào)用者。如果這是一個(gè)異步 Task 方法,Task 就會(huì)在這個(gè)時(shí)間點(diǎn)返回。但對于 async void,什么都不會(huì)返回。Time 方法只知道它調(diào)用了 action();委托調(diào)用返回;它不知道 async 方法實(shí)際上仍在“運(yùn)行”,并將在稍后異步完成。
這就是 OperationStarted/OperationCompleted 的作用。這種異步 void 方法本質(zhì)上與前面討論的 EAP 方法類似:這種方法的初始化是 void,因此需要一些其他機(jī)制來跟蹤所有此類操作。因此,EAP 實(shí)現(xiàn)在操作啟動(dòng)時(shí)調(diào)用當(dāng)前 SynchronizationContext 的 OperationStarted,在操作完成時(shí)調(diào)用 OperationCompleted,async void 也做同樣的事情。與 async void 相關(guān)的構(gòu)建器是 AsyncVoidMethodBuilder。還記得在 async 方法的入口,編譯器生成的代碼如何調(diào)用構(gòu)建器的靜態(tài) Create 方法來獲得適當(dāng)?shù)臉?gòu)建器實(shí)例嗎?AsyncVoidMethodBuilder 利用了這一點(diǎn)來掛鉤創(chuàng)建和調(diào)用 OperationStarted:
public static AsyncVoidMethodBuilder Create() { SynchronizationContext? sc = SynchronizationContext.Current; sc?.OperationStarted(); return new AsyncVoidMethodBuilder() { _synchronizationContext = sc }; }
類似地,當(dāng)通過 SetResult 或 SetException 將構(gòu)建器標(biāo)記為完成時(shí),它會(huì)調(diào)用相應(yīng)的 OperationCompleted 方法。這就是像 xunit 這樣的單元測試框架如何能夠具有異步 void 測試方法,并仍然在并發(fā)測試執(zhí)行中使用最大程度的并發(fā),例如在 xunit 的 AsyncTestSyncContext 中。
有了這些知識(shí),現(xiàn)在可以重寫我們的 timing 示例:
using System.Diagnostics; Time(async () => { Console.WriteLine("Enter"); await Task.Delay(TimeSpan.FromSeconds(10)); Console.WriteLine("Exit"); }); static void Time(Action action) { var oldCtx = SynchronizationContext.Current; try { var newCtx = new CountdownContext(); SynchronizationContext.SetSynchronizationContext(newCtx); Console.WriteLine("Timing..."); Stopwatch sw = Stopwatch.StartNew(); action(); newCtx.SignalAndWait(); Console.WriteLine($"...done timing: {sw.Elapsed}"); } finally { SynchronizationContext.SetSynchronizationContext(oldCtx); } } sealed class CountdownContext : SynchronizationContext { private readonly ManualResetEventSlim _mres = new ManualResetEventSlim(false); private int _remaining = 1; public override void OperationStarted() => Interlocked.Increment(ref _remaining); public override void OperationCompleted() { if (Interlocked.Decrement(ref _remaining) == 0) { _mres.Set(); } } public void SignalAndWait() { OperationCompleted(); _mres.Wait(); } }
在這里,我已經(jīng)創(chuàng)建了一個(gè) SynchronizationContext,它跟蹤了一個(gè)待定操作的計(jì)數(shù),并支持阻塞等待它們?nèi)客瓿?。?dāng)我運(yùn)行它時(shí),我得到這樣的輸出:
Timing...
Enter
Exit
...done timing: 00:00:10.0149074
State Machine Fields
至此,我們已經(jīng)看到了生成的入口點(diǎn)方法,以及 MoveNext 實(shí)現(xiàn)中的一切是如何工作的。我們還了解了在狀態(tài)機(jī)上定義的一些字段。讓我們仔細(xì)看看這些。
對于前面給出的 CopyStreamToStream 方法:
public async Task CopyStreamToStreamAsync(Stream source, Stream destination) { var buffer = new byte[0x1000]; int numRead; while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) { await destination.WriteAsync(buffer, 0, numRead); } }
下面是我們最終得到的字段:
private struct <CopyStreamToStreamAsync>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Stream source; public Stream destination; private byte[] <buffer>5__2; private TaskAwaiter <>u__1; private TaskAwaiter<int> <>u__2; ... }
< > 1 __state。是“狀態(tài)機(jī)”中的“狀態(tài)”。它定義了狀態(tài)機(jī)所處的當(dāng)前狀態(tài),最重要的是下次調(diào)用 MoveNext 時(shí)應(yīng)該做什么。如果狀態(tài)為-2,則操作完成。如果狀態(tài)是-1,要么是我們第一次調(diào)用 MoveNext,要么是 MoveNext 代碼正在某個(gè)線程上運(yùn)行。如果你正在調(diào)試一個(gè) async 方法的處理過程,并且你看到狀態(tài)為-1,這意味著在某處有某個(gè)線程正在執(zhí)行包含在方法中的代碼。如果狀態(tài)大于等于0,方法會(huì)被掛起,狀態(tài)的值會(huì)告訴你在什么時(shí)候掛起。雖然這不是一個(gè)嚴(yán)格的規(guī)則(某些代碼模式可能會(huì)混淆編號(hào)),但通常情況下,分配的狀態(tài)對應(yīng)于從0開始的 await 編號(hào),按照源代碼從上到下的順序排列。例如,如果 async 方法的函數(shù)體完全是:
await A(); await B(); await C(); await D();
你發(fā)現(xiàn)狀態(tài)值是2,這幾乎肯定意味著 async 方法當(dāng)前被掛起,等待從 C() 返回的任務(wù)完成。
< > t__builder。這是狀態(tài)機(jī)的構(gòu)建器,例如用于 Task 的 AsyncTaskMethodBuilder,用于 ValueTask 的 AsyncValueTaskMethodBuilder<TResult>,用于 async void 方法的 AsyncVoidMethodBuilder,或用于 async 返回類型的 AsyncMethodBuilder(…)] 或通過 async 方法本身的屬性覆蓋的任何構(gòu)建器。如前所述,構(gòu)建器負(fù)責(zé) async 方法的生命周期,包括創(chuàng)建 return 任務(wù),最終完成該任務(wù),并充當(dāng)暫停的中介,async 方法中的代碼要求構(gòu)建器暫停,直到特定的 awaiter 完成。
編譯器完全按照參數(shù)名稱的指定來命名它們。如前所述,所有被方法主體使用的參數(shù)都需要被存儲(chǔ)到狀態(tài)機(jī)中,以便 MoveNext 方法能夠訪問它們。注意我說的是 "被使用"。如果編譯器發(fā)現(xiàn)一個(gè)參數(shù)沒有被異步方法的主體使用,它就可以優(yōu)化,不需要存儲(chǔ)這個(gè)字段。例如,給定下面的方法:
public async Task M(int someArgument) { await Task.Yield(); }
編譯器會(huì)將這些字段發(fā)送到狀態(tài)機(jī):
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private YieldAwaitable.YieldAwaiter <>u__1; ... }
請注意,這里明顯缺少名為 someArgument 的參數(shù)。但是,如果我們改變 async 方法,讓它以任何方式使用實(shí)參:
public async Task M(int someArgument) { Console.WriteLine(someArgument); await Task.Yield(); }
它顯示:
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public int someArgument; private YieldAwaitable.YieldAwaiter <>u__1; ... }
<buffer>5__2;。這是緩沖區(qū)的 "局部",它被提升為一個(gè)字段,這樣它就可以在等待點(diǎn)上存活。編譯器相當(dāng)努力地防止?fàn)顟B(tài)被不必要地提升。注意,在源碼中還有一個(gè)局部變量 numRead,在狀態(tài)機(jī)中沒有相應(yīng)的字段。為什么?因?yàn)樗鼪]有必要。這個(gè)局部變量被設(shè)置為 ReadAsync 調(diào)用的結(jié)果,然后被用作 WriteAsync 調(diào)用的輸入。在這兩者之間沒有 await,因此 numRead 的值需要被存儲(chǔ)。就像在一個(gè)同步方法中,JIT 編譯器可以選擇將這樣的值完全存儲(chǔ)在一個(gè)寄存器中,而不會(huì)真正將其溢出到堆棧中,C# 編譯器可以避免將這個(gè)局部變量提升為一個(gè)字段,因?yàn)樗恍枰谌魏蔚却斜4嫠闹怠R话銇碚f,如果 C# 編譯器能夠證明局部變量的值不需要在等待中保存,它就可以省略局部變量的提升。
<>u__1和<>u__2。async 方法中有兩個(gè) await:一個(gè)用于 ReadAsync 返回的 Task<int>,另一個(gè)用于 WriteAsync 返回的 Task。Task. getawaiter() 返回一個(gè) TaskAwaiter,Task<TResult>. getawaiter() 返回一個(gè) TaskAwaiter<TResult>,兩者都是不同的結(jié)構(gòu)體類型。由于編譯器需要在 await (IsCompleted, UnsafeOnCompleted) 之前獲取這些 awaiter,然后需要在 await (GetResult) 之后訪問它們,因此需要存儲(chǔ)這些 awaiter。由于它們是不同的結(jié)構(gòu)類型,編譯器需要維護(hù)兩個(gè)單獨(dú)的字段來做到這一點(diǎn)(另一種選擇是將它們裝箱,并為 awaiter 提供一個(gè)對象字段,但這會(huì)導(dǎo)致額外的分配成本)。不過,編譯器會(huì)盡可能地重復(fù)使用字段。如果我有:
public async Task M() { await Task.FromResult(1); await Task.FromResult(true); await Task.FromResult(2); await Task.FromResult(false); await Task.FromResult(3); }
有五個(gè)等待,但只涉及兩種不同類型的等待者:三個(gè)是 TaskAwaiter<int>,兩個(gè)是 TaskAwaiter<bool>。因此,狀態(tài)機(jī)上最終只有兩個(gè)等待者字段:
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private TaskAwaiter<int> <>u__1; private TaskAwaiter<bool> <>u__2; ... }
然后,如果我將我的示例改為:
public async Task M() { await Task.FromResult(1); await Task.FromResult(true); await Task.FromResult(2).ConfigureAwait(false); await Task.FromResult(false).ConfigureAwait(false); await Task.FromResult(3); }
仍然只涉及 Task<int>s 和 Task<bool>s,但實(shí)際上我使用了四個(gè)不同的 struct awaiter 類型,因?yàn)閺?ConfigureAwait 返回的東西上的 GetAwaiter() 調(diào)用返回的 awaiter 與 Task.GetAwaiter() 返回的是不同的類型…從編譯器創(chuàng)建的 awaiter 字段可以再次很明顯的看出:
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; private TaskAwaiter<int> <>u__1; private TaskAwaiter<bool> <>u__2; private ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter <>u__3; private ConfiguredTaskAwaitable<bool>.ConfiguredTaskAwaiter <>u__4; ... }
如果您發(fā)現(xiàn)自己想要優(yōu)化與異步狀態(tài)機(jī)相關(guān)的大小,您可以查看的一件事是是否可以合并正在等待的事情,從而合并這些 awaiter 字段。
您可能還會(huì)看到在狀態(tài)機(jī)上定義的其他類型的字段。值得注意的是,您可能會(huì)看到一些字段包含單詞“wrap”??紤]下面這個(gè)例子:
public async Task<int> M() => await Task.FromResult(42) + DateTime.Now.Second;
這將生成一個(gè)包含以下字段的狀態(tài)機(jī):
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder<int> <>t__builder; private TaskAwaiter<int> <>u__1; ... }
到目前為止沒有什么特別的?,F(xiàn)在顛倒一下添加表達(dá)式的順序:
public async Task<int> M() => DateTime.Now.Second + await Task.FromResult(42);
這樣,你就得到了這些字段:
private struct <M>d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder<int> <>t__builder; private int <>7__wrap1; private TaskAwaiter<int> <>u__1; ... }
我們現(xiàn)在有了另一個(gè)函數(shù):<>7__wrap1。為什么?因?yàn)槲覀冇?jì)算了 DateTime.Now 的值。其次,只有在計(jì)算完它之后,我們才需要等待一些東西,并且需要保留第一個(gè)表達(dá)式的值,以便將其與第二個(gè)表達(dá)式的結(jié)果相加。因此,編譯器需要確保第一個(gè)表達(dá)式的臨時(shí)結(jié)果可以添加到 await 的結(jié)果中,這意味著它需要將表達(dá)式的結(jié)果溢出到臨時(shí)中,它使用 <>7__wrap1 字段做到了這一點(diǎn)。如果你發(fā)現(xiàn)自己對異步方法的實(shí)現(xiàn)進(jìn)行了超優(yōu)化,以減少分配的內(nèi)存量,你可以尋找這樣的字段,并查看對源代碼的微調(diào)是否可以避免溢出的需要,從而避免這種臨時(shí)的需要。
我希望這篇文章有助于解釋當(dāng)你使用 async/await 時(shí)背后到底發(fā)生了什么。這里有很多變化,所有這些結(jié)合在一起,創(chuàng)建了一個(gè)高效的解決方案,可以編寫可拓展的異步代碼,而不必處理回調(diào)。然而歸根結(jié)底,這些部分實(shí)際上是相對簡單的:任何異步操作的通用表示,一種能夠?qū)⑵胀刂屏髦貙憺閰f(xié)程的狀態(tài)機(jī)實(shí)現(xiàn)的語言和編譯器,以及將它們綁定在一起的模式。其他一切都是優(yōu)化的額外收獲。
以上就是 C# 語言中async/await工作原理SynchronizationContext 和 ConfigureAwait的詳細(xì)內(nèi)容,更多關(guān)于 C# 語言 async/await的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#使用StopWatch獲取程序毫秒級(jí)執(zhí)行時(shí)間的方法
這篇文章主要介紹了C#使用StopWatch獲取程序毫秒級(jí)執(zhí)行時(shí)間的方法,涉及C#操作時(shí)間的相關(guān)技巧,需要的朋友可以參考下2015-04-04Unity InputFiled TMP屬性和各種監(jiān)聽示例詳解
這篇文章主要為大家介紹了Unity InputFiled TMP屬性和各種監(jiān)聽示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01使用C#調(diào)用百度地圖并實(shí)現(xiàn)坐標(biāo)點(diǎn)的設(shè)置以及讀取示例
這篇文章主要介紹了使用C#調(diào)用百度地圖并實(shí)現(xiàn)坐標(biāo)點(diǎn)的設(shè)置以及讀取示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07C#?as?和?is?運(yùn)算符區(qū)別和用法示例解析
在C#中,as?和?is?關(guān)鍵字都用于處理類型轉(zhuǎn)換的運(yùn)算符,但它們有不同的用途和行為,本文我們將詳細(xì)解釋這兩個(gè)運(yùn)算符的區(qū)別和用法,需要的朋友可以參考下2025-01-01