亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

C#語言async?await之迭代器工作原理示例解析

 更新時間:2023年05月31日 11:59:42   作者:微軟技術棧  
這篇文章主要為大家介紹了C#語言async?await之迭代器工作原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

C# 迭代器

接《async/await 在 C# 語言中是如何工作的?(上)》,今天我們繼續(xù)介紹 C# 迭代器和 async/await under the covers。

這個解決方案的伏筆實際上是在 Task 出現(xiàn)的幾年前,即 C# 2.0,當時它增加了對迭代器的支持。

迭代器允許你編寫一個方法,然后由編譯器用來實現(xiàn) IEnumerable<T> 和/或 IEnumerator<T>。例如,如果我想創(chuàng)建一個產(chǎn)生斐波那契數(shù)列的枚舉數(shù),我可以這樣寫:

public static IEnumerable<int> Fib(){
    int prev = 0, next = 1;
    yield return prev;
    yield return next;
    while (true)
    {
        int sum = prev + next;
        yield return sum;
        prev = next;
        next = sum;
    }}

然后我可以用 foreach 枚舉它:

foreach (int i in Fib()){
    if (i > 100) break;
    Console.Write($"{i} ");}

我可以通過像 System.Linq.Enumerable 上的組合器將它與其他 IEnumerable<T> 進行組合:

foreach (int i in Fib().Take(12)){
    Console.Write($"{i} ");}

或者我可以直接通過 IEnumerator<T> 來手動枚舉它:

using IEnumerator<int> e = Fib().GetEnumerator();while (e.MoveNext()){
    int i = e.Current;
    if (i > 100) break;
    Console.Write($"{i} ");}

以上所有的結(jié)果是這樣的輸出:

0 1 1 2 3 5 8 13 21 34 55 89

真正有趣的是,為了實現(xiàn)上述目標,我們需要能夠多次進入和退出 Fib 方法。我們調(diào)用 MoveNext,它進入方法,然后該方法執(zhí)行,直到它遇到 yield return,此時對 MoveNext 的調(diào)用需要返回 true,隨后對 Current 的訪問需要返回 yield value。然后我們再次調(diào)用 MoveNext,我們需要能夠在 Fib 中從我們上次停止的地方開始,并且保持上次調(diào)用的所有狀態(tài)不變。迭代器實際上是由 C# 語言/編譯器提供的協(xié)程,編譯器將 Fib 迭代器擴展為一個成熟的狀態(tài)機。

所有關于 Fib 的邏輯現(xiàn)在都在 MoveNext 方法中,但是作為跳轉(zhuǎn)表的一部分,它允許實現(xiàn)分支到它上次離開的位置,這在枚舉器類型上生成的狀態(tài)字段中被跟蹤。而我寫的局部變量,如 prev、next 和 sum,已經(jīng)被 "提升 "為枚舉器上的字段,這樣它們就可以在調(diào)用 MoveNext 時持續(xù)存在。

在我之前的例子中,我展示的最后一種枚舉形式涉及手動使用 IEnumerator<T>。在那個層面上,我們手動調(diào)用 MoveNext(),決定何時是重新進入循環(huán)程序的適當時機。但是,如果不這樣調(diào)用它,而是讓 MoveNext 的下一次調(diào)用實際成為異步操作完成時執(zhí)行的延續(xù)工作的一部分呢?如果我可以 yield 返回一些代表異步操作的東西,并讓消耗代碼將 continuation 連接到該 yield 對象,然后在該 continuation 執(zhí)行 MoveNext 時會怎么樣?使用這種方法,我可以編寫一個輔助方法:

static Task IterateAsync(IEnumerable<Task> tasks){
    var tcs = new TaskCompletionSource();
    IEnumerator<Task> e = tasks.GetEnumerator();
    void Process()
    {
        try
        {
            if (e.MoveNext())
            {
                e.Current.ContinueWith(t => Process());
                return;
            }
        }
        catch (Exception e)
        {
            tcs.SetException(e);
            return;
        }
        tcs.SetResult();
    };
    Process();
    return tcs.Task;}

現(xiàn)在變得有趣了。我們得到了一個可迭代的任務列表。每次我們 MoveNext 到下一個 Task 并獲得一個時,我們將該任務的 continuation 連接起來;當這個 Task 完成時,它只會回過頭來調(diào)用執(zhí)行 MoveNext、獲取下一個 Task 的相同邏輯,以此類推。這是建立在將 Task 作為任何異步操作的單一表示的思想之上的,所以我們輸入的枚舉表可以是一個任何異步操作的序列。這樣的序列是從哪里來的呢?當然是通過迭代器。

還記得我們之前的 CopyStreamToStream 例子嗎?考慮一下這個:

static Task CopyStreamToStreamAsync(Stream source, Stream destination){
    return IterateAsync(Impl(source, destination));
    static IEnumerable<Task> Impl(Stream source, Stream destination)
    {
        var buffer = new byte[0x1000];
        while (true)
        {
            Task<int> read = source.ReadAsync(buffer, 0, buffer.Length);
            yield return read;
            int numRead = read.Result;
            if (numRead <= 0)
            {
                break;
            }
            Task write = destination.WriteAsync(buffer, 0, numRead);
            yield return write;
            write.Wait();
        }
    }}

我們正在調(diào)用那個 IterateAsync 助手,而我們提供給它的枚舉表是由一個處理所有控制流的迭代器產(chǎn)生的。它調(diào)用 Stream.ReadAsync 然后 yield 返回 Task;yield task 在調(diào)用 MoveNext 之后會被傳遞給 IterateAsync,而 IterateAsync 會將一個 continuation 掛接到那個 task 上,當它完成時,它會回調(diào) MoveNext 并在 yield 之后回到這個迭代器。此時,Impl 邏輯獲得方法的結(jié)果,調(diào)用 WriteAsync,并再次生成它生成的 Task。以此類推。

這就是 C# 和 .NET 中 async/await 的開始。

在 C# 編譯器中支持迭代器和 async/await 的邏輯中,大約有95%左右的邏輯是共享的。不同的語法,不同的類型,但本質(zhì)上是相同的轉(zhuǎn)換。

事實上,在 async/await 出現(xiàn)之前,一些開發(fā)人員就以這種方式使用迭代器進行異步編程。在實驗性的 Axum 編程語言中也有類似的轉(zhuǎn)換原型,這是 C# 支持異步的關鍵靈感來源。Axum 提供了一個可以放在方法上的 async 關鍵字,就像 C# 中的 async 一樣。

Task 還不普遍,所以在異步方法中,Axum 編譯器啟發(fā)式地將同步方法調(diào)用與 APM 對應的方法相匹配,例如,如果它看到你調(diào)用 stream.Read,它會找到并利用相應的 stream.BeginRead 和 stream.EndRead 方法,合成適當?shù)奈袀鬟f給 Begin 方法,同時還為定義為可組合的 async 方法生成完整的 APM 實現(xiàn)。它甚至還集成了 SynchronizationContext!雖然 Axum 最終被擱置,但它為 C# 中的 async/await 提供了一個很棒的原型。

async/await under the covers

現(xiàn)在我們知道了我們是如何做到這一點的,讓我們深入研究它實際上是如何工作的。作為參考,下面是我們的同步方法示例:

public void CopyStreamToStream(Stream source, Stream destination){
    var buffer = new byte[0x1000];
    int numRead;
    while ((numRead = source.Read(buffer, 0, buffer.Length)) != 0)
    {
        destination.Write(buffer, 0, numRead);
    }}

下面是 async/await 對應的方法:

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

簽名從 void 變成了 async Task,我們分別調(diào)用了 ReadAsync 和 WriteAsync,而不是 Read 和 Write,這兩個操作都帶 await 前綴。編譯器和核心庫接管了其余部分,從根本上改變了代碼實際執(zhí)行的方式。讓我們深入了解一下是如何做到的。

編譯器轉(zhuǎn)換

我們已經(jīng)看到,和迭代器一樣,編譯器基于狀態(tài)機重寫了 async 方法。我們?nèi)匀挥幸粋€與開發(fā)人員寫的簽名相同的方法(public Task CopyStreamToStreamAsync(Stream source, Stream destination)),但該方法的主體完全不同:

[AsyncStateMachine(typeof(<CopyStreamToStreamAsync>d__0))]public Task CopyStreamToStreamAsync(Stream source, Stream destination){
    <CopyStreamToStreamAsync>d__0 stateMachine = default;
    stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.source = source;
    stateMachine.destination = destination;
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;}
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;
    ...}

注意,與開發(fā)人員所寫的簽名的唯一區(qū)別是缺少 async 關鍵字本身。Async 實際上不是方法簽名的一部分;就像 unsafe 一樣,當你把它放在方法簽名中,你是在表達方法的實現(xiàn)細節(jié),而不是作為契約的一部分實際公開出來的東西。使用 async/await 實現(xiàn) task -return 方法是實現(xiàn)細節(jié)。

編譯器已經(jīng)生成了一個名為 <CopyStreamToStreamAsync>d__0 的結(jié)構(gòu)體,并且它在堆棧上對該結(jié)構(gòu)體的實例進行了零初始化。重要的是,如果異步方法同步完成,該狀態(tài)機將永遠不會離開堆棧。這意味著沒有與狀態(tài)機相關的分配,除非該方法需要異步完成,也就是說它需要等待一些尚未完成的任務。稍后會有更多關于這方面的內(nèi)容。

該結(jié)構(gòu)體是方法的狀態(tài)機,不僅包含開發(fā)人員編寫的所有轉(zhuǎn)換邏輯,還包含用于跟蹤該方法中當前位置的字段,以及編譯器從方法中提取的所有“本地”狀態(tài),這些狀態(tài)需要在 MoveNext 調(diào)用之間生存。它在邏輯上等價于迭代器中的 IEnumerable<T>/IEnumerator<T> 實現(xiàn)。(請注意,我展示的代碼來自發(fā)布版本;在調(diào)試構(gòu)建中,C# 編譯器將實際生成這些狀態(tài)機類型作為類,因為這樣做可以幫助某些調(diào)試工作)。

在初始化狀態(tài)機之后,我們看到對 AsyncTaskMethodBuilder.Create() 的調(diào)用。雖然我們目前關注的是 Tasks,但 C# 語言和編譯器允許從異步方法返回任意類型(“task-like”類型),例如,我可以編寫一個方法 public async MyTask CopyStreamToStreamAsync,只要我們以適當?shù)姆绞綌U展我們前面定義的 MyTask,它就能順利編譯。這種適當性包括聲明一個相關的“builder”類型,并通過 AsyncMethodBuilder 屬性將其與該類型關聯(lián)起來:

[AsyncMethodBuilder(typeof(MyTaskMethodBuilder))]public class MyTask{
    ...}
public struct MyTaskMethodBuilder{
    public static MyTaskMethodBuilder Create() { ... }
    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { ... }
    public void SetStateMachine(IAsyncStateMachine stateMachine) { ... }
    public void SetResult() { ... }
    public void SetException(Exception exception) { ... }
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine { ... }
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine { ... }
    public MyTask Task { get { ... } }}

在這種情況下,這樣的“builder”知道如何創(chuàng)建該類型的實例(Task 屬性),如何成功完成并在適當?shù)那闆r下有結(jié)果(SetResult)或有異常(SetException),以及如何處理連接等待尚未完成的事務的延續(xù)(AwaitOnCompleted/AwaitUnsafeOnCompleted)。在 System.Threading.Tasks.Task 的情況下,它默認與 AsyncTaskMethodBuilder 相關聯(lián)。通常情況下,這種關聯(lián)是通過應用在類型上的 [AsyncMethodBuilder(…)] 屬性提供的,但在 C# 中,Task 是已知的,因此實際上沒有該屬性。因此,編譯器已經(jīng)讓構(gòu)建器使用這個異步方法,并使用模式中的 Create 方法構(gòu)建它的實例。請注意,與狀態(tài)機一樣,AsyncTaskMethodBuilder 也是一個結(jié)構(gòu)體,因此這里也沒有內(nèi)存分配。

然后用這個入口點方法的參數(shù)填充狀態(tài)機。這些參數(shù)需要能夠被移動到 MoveNext 中的方法體訪問,因此這些參數(shù)需要存儲在狀態(tài)機中,以便后續(xù)調(diào)用 MoveNext 時代碼可以引用它們。該狀態(tài)機也被初始化為初始-1狀態(tài)。如果 MoveNext 被調(diào)用且狀態(tài)為-1,那么邏輯上我們將從方法的開始處開始。

現(xiàn)在是最不顯眼但最重要的一行:調(diào)用構(gòu)建器的 Start 方法。這是模式的另一部分,必須在 async 方法的返回位置所使用的類型上公開,它用于在狀態(tài)機上執(zhí)行初始的 MoveNext。構(gòu)建器的 Start 方法實際上是這樣的:

public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine{
    stateMachine.MoveNext();}

例如,調(diào)用 stateMachine.<>t__builder.Start(ref stateMachine); 實際上只是調(diào)用 stateMachine.MoveNext()。在這種情況下,為什么編譯器不直接發(fā)出這個信號呢?為什么還要有 Start 呢?答案是,Start 的內(nèi)容比我所說的要多一點。但為此,我們需要簡單地了解一下 ExecutionContext。

? ExecutionContext

我們都熟悉在方法之間傳遞狀態(tài)。調(diào)用一個方法,如果該方法指定了形參,就使用實參調(diào)用該方法,以便將該數(shù)據(jù)傳遞給被調(diào)用方。這是顯式傳遞數(shù)據(jù)。但還有其他更隱蔽的方法。例如,方法可以是無參數(shù)的,但可以指定在調(diào)用方法之前填充某些特定的靜態(tài)字段,然后從那里獲取狀態(tài)。這個方法的簽名中沒有任何東西表明它接收參數(shù),因為它確實沒有:只是調(diào)用者和被調(diào)用者之間有一個隱含的約定,即調(diào)用者可能填充某些內(nèi)存位置,而被調(diào)用者可能讀取這些內(nèi)存位置。被調(diào)用者和調(diào)用者甚至可能沒有意識到它的發(fā)生,如果他們是中介,方法 A 可能填充靜態(tài)信息,然后調(diào)用 B, B 調(diào)用 C, C 調(diào)用 D,最終調(diào)用 E,讀取這些靜態(tài)信息的值。這通常被稱為“環(huán)境”數(shù)據(jù):它不是通過參數(shù)傳遞給你的,而是掛在那里,如果需要的話,你可以使用。

我們可以更進一步,使用線程局部狀態(tài)。線程局部狀態(tài),在 .NET 中是通過屬性為 [ThreadStatic] 的靜態(tài)字段或通過 ThreadLocal<T> 類型實現(xiàn)的,可以以相同的方式使用,但數(shù)據(jù)僅限于當前執(zhí)行的線程,每個線程都能夠擁有這些字段的自己的隔離副本。這樣,您就可以填充線程靜態(tài),進行方法調(diào)用,然后在方法完成后將更改還原到線程靜態(tài),從而啟用這種隱式傳遞數(shù)據(jù)的完全隔離形式。

如果我們進行異步方法調(diào)用,而異步方法中的邏輯想要訪問環(huán)境數(shù)據(jù),它會怎么做?如果數(shù)據(jù)存儲在常規(guī)靜態(tài)中,異步方法將能夠訪問它,但一次只能有一個這樣的方法在運行,因為多個調(diào)用者在寫入這些共享靜態(tài)字段時可能會覆蓋彼此的狀態(tài)。如果數(shù)據(jù)存儲在線程靜態(tài)信息中,異步方法將能夠訪問它,但只有在調(diào)用線程停止同步運行之前;如果它將 continuation 連接到它發(fā)起的某個操作,并且該 continuation 最終在某個其他線程上運行,那么它將不再能夠訪問線程靜態(tài)信息。即使它碰巧運行在同一個線程上,無論是偶然的還是由于調(diào)度器的強制,在它這樣做的時候,數(shù)據(jù)可能已經(jīng)被該線程發(fā)起的其他操作刪除和/或覆蓋。對于異步,我們需要一種機制,允許任意環(huán)境數(shù)據(jù)在這些異步點上流動,這樣在 async 方法的整個邏輯中,無論何時何地運行,它都可以訪問相同的數(shù)據(jù)。

輸入 ExecutionContext。ExecutionContext 類型是異步操作和異步操作之間傳遞環(huán)境數(shù)據(jù)的媒介。它存在于一個 [ThreadStatic] 中,但是當某些異步操作啟動時,它被“捕獲”(從該線程靜態(tài)中讀取副本的一種奇特的方式),存儲,然后當該異步操作的延續(xù)被運行時,ExecutionContext 首先被恢復到即將運行該操作的線程中的 [ThreadStatic] 中。ExecutionContext 是實現(xiàn) AsyncLocal<T> 的機制(事實上,在 .NET Core 中,ExecutionContext 完全是關于 AsyncLocal<T> 的,僅此而已),例如,如果你將一個值存儲到 AsyncLocal<T> 中,然后例如隊列一個工作項在 ThreadPool 上運行,該值將在該 AsyncLocal<T> 中可見,在該工作項上運行:

var number = new AsyncLocal<int>();
number.Value = 42;ThreadPool.QueueUserWorkItem(_ => Console.WriteLine(number.Value));
number.Value = 0;
Console.ReadLine();

這段代碼每次運行時都會打印42。在我們對委托進行排隊之后,我們將 AsyncLocal<int> 的值重置為0,這無關緊要,因為 ExecutionContext 是作為 QueueUserWorkItem 調(diào)用的一部分被捕獲的,而該捕獲包含了當時 AsyncLocal<int> 的狀態(tài)。

? Back To Start

當我在寫 AsyncTaskMethodBuilder.Start 的實現(xiàn)時,我們繞道討論了 ExecutionContext,我說這是有效的:

public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine{
    stateMachine.MoveNext();}

然后建議我簡化一下。這種簡化忽略了一個事實,即該方法實際上需要將 ExecutionContext 考慮在內(nèi),因此更像是這樣:

public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine{
    ExecutionContext previous = Thread.CurrentThread._executionContext; // [ThreadStatic] field
    try
    {
        stateMachine.MoveNext();
    }
    finally
    {
        ExecutionContext.Restore(previous); // internal helper
    }}

這里不像我之前建議的那樣只調(diào)用 statemmachine .MoveNext(),而是在這里做了一個動作:獲取當前的 ExecutionContext,再調(diào)用 MoveNext,然后在它完成時將當前上下文重置為調(diào)用 MoveNext 之前的狀態(tài)。

這樣做的原因是為了防止異步方法將環(huán)境數(shù)據(jù)泄露給調(diào)用者。一個示例方法說明了為什么這很重要:

async Task ElevateAsAdminAndRunAsync(){
    using (WindowsIdentity identity = LoginAdmin())
    {
        using (WindowsImpersonationContext impersonatedUser = identity.Impersonate())
        {
            await DoSensitiveWorkAsync();
        }
    }}

“冒充”是將當前用戶的環(huán)境信息改為其他人的;這讓代碼可以代表其他人,使用他們的特權(quán)和訪問權(quán)限。在 .NET 中,這種模擬跨異步操作流動,這意味著它是 ExecutionContext 的一部分?,F(xiàn)在想象一下,如果 Start 沒有恢復之前的上下文,考慮下面的代碼:

Task t = ElevateAsAdminAndRunAsync();PrintUser();await t;

這段代碼可以發(fā)現(xiàn),ElevateAsAdminAndRunAsync 中修改的 ExecutionContext 在 ElevateAsAdminAndRunAsync 返回到它的同步調(diào)用者之后仍然存在(這發(fā)生在該方法第一次等待尚未完成的內(nèi)容時)。這是因為在調(diào)用 Impersonate 之后,我們調(diào)用了 DoSensitiveWorkAsync 并等待它返回的任務。假設任務沒有完成,它將導致對 ElevateAsAdminAndRunAsync 的調(diào)用 yield 并返回到調(diào)用者,模擬仍然在當前線程上有效。這不是我們想要的。因此,Start 設置了這個保護機制,以確保對 ExecutionContext 的任何修改都不會從同步方法調(diào)用中流出,而只會隨著方法執(zhí)行的任何后續(xù)工作一起流出。

? MoveNext

因此,調(diào)用了入口點方法,初始化了狀態(tài)機結(jié)構(gòu)體,調(diào)用了 Start,然后調(diào)用了 MoveNext。什么是 MoveNext?這個方法包含了開發(fā)者方法中所有的原始邏輯,但做了一大堆修改。讓我們先看看這個方法的腳手架。下面是編譯器為我們的方法生成的反編譯版本,但刪除了生成的 try 塊中的所有內(nèi)容:

private void MoveNext(){
    try
    {
        ... // all of the code from the CopyStreamToStreamAsync method body, but not exactly as it was written
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <buffer>5__2 = null;
        <>t__builder.SetException(exception);
        return;
    }
    <>1__state = -2;
    <buffer>5__2 = null;
    <>t__builder.SetResult();}

無論 MoveNext 執(zhí)行什么其他工作,當所有工作完成后,它都有責任完成 async Task 方法返回的任務。如果 try 代碼塊的主體拋出了未處理的異常,那么任務就會拋出該異常。如果 async 方法成功到達它的終點(相當于同步方法返回),它將成功完成返回的任務。在任何一種情況下,它都將設置狀態(tài)機的狀態(tài)以表示完成。(我有時聽到開發(fā)人員從理論上說,當涉及到異常時,在第一個 await 之前拋出的異常和在第一個 await 之后拋出的異常是有區(qū)別的……基于上述,應該清楚情況并非如此。任何未在 async 方法中處理的異常,不管它在方法的什么位置,也不管方法是否產(chǎn)生了結(jié)果,都會在上面的 catch 塊中結(jié)束,然后被捕獲的異常會存儲在 async 方法返回的任務中。)

還要注意,這個完成過程是通過構(gòu)建器完成的,使用它的 SetException 和 SetResult 方法,這是編譯器預期的構(gòu)建器模式的一部分。如果 async 方法之前已經(jīng)掛起了,那么構(gòu)建器將不得不再掛起處理中創(chuàng)建一個 Task (稍后我們會看到如何以及在哪里執(zhí)行),在這種情況下,調(diào)用 SetException/SetResult 將完成該任務。然而,如果 async 方法之前沒有掛起,那么我們還沒有創(chuàng)建任務或向調(diào)用者返回任何東西,因此構(gòu)建器在生成任務時有更大的靈活性。如果你還記得之前在入口點方法中,它做的最后一件事是將任務返回給調(diào)用者,它通過訪問構(gòu)建器的 Task 屬性返回結(jié)果:

public Task CopyStreamToStreamAsync(Stream source, Stream destination)
{
    ...
    return stateMachine.<>t__builder.Task;
}

構(gòu)建器知道該方法是否掛起過,如果掛起了,它就會返回已經(jīng)創(chuàng)建的任務。如果方法從未掛起,而且構(gòu)建器還沒有任務,那么它可以在這里創(chuàng)建一個完成的任務。在成功完成的情況下,它可以直接使用 Task.CompletedTask 而不是分配一個新的任務,避免任何分配。如果是一般的任務 <TResult>,構(gòu)建者可以直接使用 Task.FromResult<TResult>(TResult result)。

構(gòu)建器還可以對它創(chuàng)建的對象進行任何它認為合適的轉(zhuǎn)換。例如,Task 實際上有三種可能的最終狀態(tài):成功、失敗和取消。AsyncTaskMethodBuilder 的 SetException 方法處理特殊情況 OperationCanceledException,將任務轉(zhuǎn)換為 TaskStatus。如果提供的異常是 OperationCanceledException 或源自 OperationCanceledException,則將任務轉(zhuǎn)換為 TaskStatus.Canceled 最終狀態(tài);否則,任務以 TaskStatus.Faulted 結(jié)束;這種區(qū)別在使用代碼時往往不明顯;因為無論異常被標記為取消還是故障,都會被存儲到 Task 中,等待該任務的代碼將無法觀察到狀態(tài)之間的區(qū)別(無論哪種情況,原始異常都會被傳播)...... 它只影響與任務直接交互的代碼,例如通過 ContinueWith,它具有重載,允許僅為完成狀態(tài)的子集調(diào)用 continuation。

現(xiàn)在我們了解了生命周期方面的內(nèi)容,下面是在 MoveNext 的 try 塊內(nèi)填寫的所有內(nèi)容:

private void MoveNext()
{
    try
    {
        int num = <>1__state;
        TaskAwaiter<int> awaiter;
        if (num != 0)
        {
            if (num != 1)
            {
                <buffer>5__2 = new byte[4096];
                goto IL_008b;
            }
            awaiter = <>u__2;
            <>u__2 = default(TaskAwaiter<int>);
            num = (<>1__state = -1);
            goto IL_00f0;
        }
        TaskAwaiter awaiter2 = <>u__1;
        <>u__1 = default(TaskAwaiter);
        num = (<>1__state = -1);
        IL_0084:
        awaiter2.GetResult();
        IL_008b:
        awaiter = source.ReadAsync(<buffer>5__2, 0, <buffer>5__2.Length).GetAwaiter();
        if (!awaiter.IsCompleted)
        {
            num = (<>1__state = 1);
            <>u__2 = awaiter;
            <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
            return;
        }
        IL_00f0:
        int result;
        if ((result = awaiter.GetResult()) != 0)
        {
            awaiter2 = destination.WriteAsync(<buffer>5__2, 0, result).GetAwaiter();
            if (!awaiter2.IsCompleted)
            {
                num = (<>1__state = 0);
                <>u__1 = awaiter2;
                <>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
                return;
            }
            goto IL_0084;
        }
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <buffer>5__2 = null;
        <>t__builder.SetException(exception);
        return;
    }
    <>1__state = -2;
    <buffer>5__2 = null;
    <>t__builder.SetResult();
}

這種復雜的情況可能感覺有點熟悉。還記得我們基于 APM 手動實現(xiàn)的 BeginCopyStreamToStream 有多復雜嗎?這沒有那么復雜,但也更好,因為編譯器為我們做了這些工作,以延續(xù)傳遞的形式重寫了方法,同時確保為這些延續(xù)保留了所有必要的狀態(tài)。即便如此,我們也可以瞇著眼睛跟著走。請記住,狀態(tài)在入口點被初始化為-1。然后我們進入 MoveNext,發(fā)現(xiàn)這個狀態(tài)(現(xiàn)在存儲在本地 num 中)既不是0也不是1,因此執(zhí)行創(chuàng)建臨時緩沖區(qū)的代碼,然后跳轉(zhuǎn)到標簽 IL_008b,在這里調(diào)用 stream.ReadAsync。注意,在這一點上,我們?nèi)匀粡恼{(diào)用 MoveNext 同步運行,因此從開始到入口點都同步運行,這意味著開發(fā)者的代碼調(diào)用了 CopyStreamToStreamAsync,它仍然在同步執(zhí)行,還沒有返回一個 Task 來表示這個方法的最終完成。

我們調(diào)用 Stream.ReadAsync,從中得到一個 Task<int>。讀取可能是同步完成的,也可能是異步完成的,但速度快到現(xiàn)在已經(jīng)完成,也可能還沒有完成。不管怎么說,我們有一個表示最終完成的 Task<int>,編譯器發(fā)出的代碼會檢查該 Task<int> 以決定如何繼續(xù):如果該 Task<int> 確實已經(jīng)完成(不管它是同步完成還是只是在我們檢查時完成),那么這個方法的代碼就可以繼續(xù)同步運行......當我們可以在這里繼續(xù)運行時,沒有必要花不必要的開銷排隊處理該方法執(zhí)行的剩余部分。但是為了處理 Task<int> 還沒有完成的情況,編譯器需要發(fā)出代碼來為 Task 掛上一個延續(xù)。因此,它需要發(fā)出代碼,詢問任務 "你完成了嗎?" 它是否是直接與任務對話來問這個問題?

如果你在 C# 中唯一可以等待的東西是 System.Threading.Tasks.Task,這將是一種限制。同樣地,如果 C# 編譯器必須知道每一種可能被等待的類型,那也是一種限制。相反,C# 在這種情況下通常會做的是:它采用了一種 api 模式。代碼可以等待任何公開適當模式(“awaiter”模式)的東西(就像您可以等待任何提供適當?shù)?ldquo;可枚舉”模式的東西一樣)。例如,我們可以增強前面寫的 MyTask 類型來實現(xiàn) awaiter 模式:

class MyTask
{
    ...
    public MyTaskAwaiter GetAwaiter() => new MyTaskAwaiter { _task = this };
    public struct MyTaskAwaiter : ICriticalNotifyCompletion
    {
        internal MyTask _task;
        public bool IsCompleted => _task._completed;
        public void OnCompleted(Action continuation) => _task.ContinueWith(_ => continuation());
        public void UnsafeOnCompleted(Action continuation) => _task.ContinueWith(_ => continuation());
        public void GetResult() => _task.Wait();
    }
}

如果一個類型公開了 getwaiter() 方法,就可以等待它,Task 就是這樣做的。這個方法需要返回一些內(nèi)容,而這些內(nèi)容又公開了幾個成員,包括一個 IsCompleted 屬性,用于在調(diào)用 IsCompleted 時檢查操作是否已經(jīng)完成。你可以看到正在發(fā)生的事情:在 IL_008b,從 ReadAsync 返回的任務已經(jīng)調(diào)用了 getwaiter,然后在 struct awaiter 實例上完成訪問。如果 IsCompleted 返回 true,那么最終會執(zhí)行到 IL_00f0,在這里代碼會調(diào)用 awaiter 的另一個成員:GetResult()。如果操作失敗,GetResult() 負責拋出異常,以便將其傳播到 async 方法中的 await 之外;否則,GetResult() 負責返回操作的結(jié)果。在 ReadAsync 的例子中,如果結(jié)果為0,那么我們跳出讀寫循環(huán),到方法的末尾調(diào)用 SetResult,就完成了。

不過,回過頭來看一下,真正有趣的部分是,如果 IsCompleted 檢查實際上返回 false,會發(fā)生什么。如果它返回 true,我們就繼續(xù)處理循環(huán),類似于在 APM 模式中 completedsynchronized 返回 true,Begin 方法的調(diào)用者負責繼續(xù)執(zhí)行,而不是回調(diào)函數(shù)。但是如果 IsCompleted 返回 false,我們需要暫停 async 方法的執(zhí)行,直到 await 操作完成。這意味著從 MoveNext 中返回,因為這是 Start 的一部分,我們?nèi)匀辉谌肟邳c方法中,這意味著將任務返回給調(diào)用者。但在發(fā)生任何事情之前,我們需要將 continuation 連接到正在等待的任務(注意,為了避免像在 APM 情況中那樣的 stack dives,如果異步操作在 IsCompleted 返回 false 后完成,但在我們連接 continuation 之前,continuation 仍然需要從調(diào)用線程異步調(diào)用,因此它將進入隊列)。由于我們可以等待任何東西,我們不能直接與任務實例對話;相反,我們需要通過一些基于模式的方法來執(zhí)行此操作。

Awaiter 公開了一個方法來連接 continuation。編譯器可以直接使用它,除了一個非常關鍵的問題:continuation 到底應該是什么?更重要的是,它應該與什么對象相關聯(lián)?請記住,狀態(tài)機結(jié)構(gòu)體在棧上,我們當前運行的 MoveNext 調(diào)用是對該實例的方法調(diào)用。我們需要保存狀態(tài)機,以便在恢復時我們擁有所有正確的狀態(tài),這意味著狀態(tài)機不能一直存在于棧中;它需要被復制到堆上的某個地方,因為棧最終將被用于該線程執(zhí)行的其他后續(xù)的、無關的工作。然后,延續(xù)需要在堆上的狀態(tài)機副本上調(diào)用 MoveNext 方法。

此外,ExecutionContext 也與此相關。狀態(tài)機需要確保存儲在 ExecutionContext 中的任何環(huán)境數(shù)據(jù)在暫停時被捕獲,然后在恢復時被應用,這意味著延續(xù)也需要合并該 ExecutionContext。因此,僅僅在狀態(tài)機上創(chuàng)建一個指向 MoveNext 的委托是不夠的。這也是我們不想要的開銷。如果當我們掛起時,我們在狀態(tài)機上創(chuàng)建了一個指向 MoveNext 的委托,那么每次這樣做我們都要對狀態(tài)機結(jié)構(gòu)進行裝箱(即使它已經(jīng)作為其他對象的一部分在堆上)并分配一個額外的委托(委托的這個對象引用將是該結(jié)構(gòu)體的一個新裝箱的副本)。因此,我們需要做一個復雜的動作,即確保我們只在方法第一次暫停執(zhí)行時將該結(jié)構(gòu)從堆棧中提升到堆中,而在其他時候都使用相同的堆對象作為 MoveNext 的目標,并在這個過程中確保我們捕獲了正確的上下文,并在恢復時確保我們使用捕獲的上下文來調(diào)用該操作。

你可以在 C# 編譯器生成的代碼中看到,當我們需要掛起時就會發(fā)生:

if (!awaiter.IsCompleted) // we need to suspend when IsCompleted is false
{
    <>1__state = 1;
    <>u__2 = awaiter;
    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
    return;
}

我們將狀態(tài) id 存儲到 state 字段中,該 id 表示當方法恢復時應該跳轉(zhuǎn)到的位置。然后,我們將 awaiter 本身持久化到一個字段中,以便在恢復后可以使用它來調(diào)用 GetResult。然后在返回 MoveNext 調(diào)用之前,我們要做的最后一件事是調(diào)用 <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this),要求構(gòu)建器為這個狀態(tài)機連接一個 continuation 到 awaiter。

(注意,它調(diào)用構(gòu)建器的 AwaitUnsafeOnCompleted 而不是構(gòu)建器的 AwaitOnCompleted,因為 awaiter 實現(xiàn)了 iccriticalnotifycompletion;狀態(tài)機處理流動的 ExecutionContext,所以我們不需要 awaiter,正如前面提到的,這樣做只會帶來重復和不必要的開銷。)

AwaitUnsafeOnCompleted 方法的實現(xiàn)太復雜了,不能在這里詳述,所以我將總結(jié)它在 .NET Framework 上的作用:

1.它使用 ExecutionContext.Capture() 來獲取當前上下文。

2.然后它分配一個 MoveNextRunner 對象來包裝捕獲的上下文和裝箱的狀態(tài)機(如果這是該方法第一次掛起,我們還沒有狀態(tài)機,所以我們只使用 null 作為占位符)。

3.然后,它創(chuàng)建一個操作委托給該 MoveNextRunner 上的 Run 方法;這就是它如何能夠獲得一個委托,該委托將在捕獲的 ExecutionContext 的上下文中調(diào)用狀態(tài)機的 MoveNext。

4.如果這是該方法第一次掛起,我們還沒有裝箱的狀態(tài)機,所以此時它會將其裝箱,通過將實例存儲到本地類型的 IAsyncStateMachine 接口中,在堆上創(chuàng)建一個副本。然后,這個盒子會被存儲到已分配的 MoveNextRunner 中。

5.現(xiàn)在到了一個有些令人費解的步驟。如果您查看狀態(tài)機結(jié)構(gòu)體的定義,它包含構(gòu)建器,public AsyncTaskMethodBuilder <>t__builder;,如果你查看構(gòu)建器的定義,它包含內(nèi)部的 IAsyncStateMachine m_stateMachine;。

構(gòu)建器需要引用裝箱的狀態(tài)機,以便在后續(xù)的掛起中它可以看到它已經(jīng)裝箱了狀態(tài)機,并且不需要再次這樣做。但是我們只是裝箱了狀態(tài)機,并且該狀態(tài)機包含一個 m_stateMachine 字段為 null 的構(gòu)建器。我們需要改變裝箱狀態(tài)機的構(gòu)建器的 m_stateMachine 指向它的父容器。

為了實現(xiàn)這一點,編譯器生成的狀態(tài)機結(jié)構(gòu)體實現(xiàn)了 IAsyncStateMachine 接口,其中包括一個 void SetStateMachine(IAsyncStateMachine stateMachine) ;方法,該狀態(tài)機結(jié)構(gòu)體包含了該接口方法的實現(xiàn):

private void SetStateMachine(IAsyncStateMachine stateMachine) =>
<>t__builder.SetStateMachine(stateMachine);

因此,構(gòu)建器對狀態(tài)機進行裝箱,然后將裝箱傳遞給裝箱的 SetStateMachine 方法,該方法會調(diào)用構(gòu)建器的 SetStateMachine 方法,將裝箱存儲到字段中。

6.最后,我們有一個表示 continuation 的 Action,它被傳遞給 awaiter 的 UnsafeOnCompleted 方法。在 TaskAwaiter 的情況下,任務將將該操作存儲到任務的 continuation 列表中,這樣當任務完成時,它將調(diào)用該操作,通過 MoveNextRunner.Run 回調(diào),通過 ExecutionContext.Run 回調(diào),最后調(diào)用狀態(tài)機的 MoveNext 方法重新進入狀態(tài)機,并從它停止的地方繼續(xù)運行。

這就是在 .NET Framework 中發(fā)生的事情,你可以在分析器中看到結(jié)果,例如通過運行分配分析器來查看每個 await 上的分配情況。讓我們看看這個愚蠢的程序,我寫這個程序只是為了強調(diào)其中涉及的分配成本:

using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        var al = new AsyncLocal<int>() { Value = 42 };
        for (int i = 0; i < 1000; i++)
        {
            await SomeMethodAsync();
        }
    }
    static async Task SomeMethodAsync()
    {
        for (int i = 0; i < 1000; i++)
        {
            await Task.Yield();
        }
    }
}

這個程序創(chuàng)建了一個 AsyncLocal<int>,讓值42通過所有后續(xù)的異步操作。然后它調(diào)用 SomeMethodAsync 1000次,每次暫停/恢復1000次。在 Visual Studio 中,我使用  .NET Object Allocation Tracking profiler 運行它,結(jié)果如下:

那是很多的分配!讓我們來研究一下它們的來源。

ExecutionContext。有超過一百萬個這樣的內(nèi)容被分配。為什么?因為在 .NET Framework 中,ExecutionContext 是一個可變的數(shù)據(jù)結(jié)構(gòu)。由于我們希望流轉(zhuǎn)一個異步操作被 fork 時的數(shù)據(jù),并且我們不希望它在 fork 之后看到執(zhí)行的變更,我們需要復制 ExecutionContext。每個單獨的 fork 操作都需要這樣的副本,因此有1000次對 SomeMethodAsync 的調(diào)用,每個調(diào)用都會暫停/恢復1000次,我們有100萬個 ExecutionContext 實例。

Action。類似地,每次我們等待尚未完成的任務時(我們的百萬個 await Task.Yield()s就是這種情況),我們最終分配一個新的操作委托來傳遞給 awaiter 的 UnsafeOnCompleted 方法。

MoveNextRunner。同樣的,有一百萬個這樣的例子,因為在前面的步驟大綱中,每次我們暫停時,我們都要分配一個新的 MoveNextRunner 來存儲 Action和 ExecutionContext,以便使用后者來執(zhí)行前者。

LogicalCallContext。這些是 .NET Framework 上 AsyncLocal<T> 的實現(xiàn)細節(jié);AsyncLocal<T> 將其數(shù)據(jù)存儲到 ExecutionContext 的“邏輯調(diào)用上下文”中,這是表示與 ExecutionContext 一起流動的一般狀態(tài)的一種奇特方式。如果我們要復制一百萬個 ExecutionContext,我們也會復制一百萬個 LogicalCallContext。

QueueUserWorkItemCallback。每個 Task.Yield() 都將一個工作項排隊到線程池中,導致分配了100萬個工作項對象用于表示這100萬個操作。

Task< VoidResult >。這里有一千個這樣的,所以至少我們脫離了"百萬"俱樂部。每個異步完成的異步任務調(diào)用都需要分配一個新的 Task 實例來表示該調(diào)用的最終完成。

< SomeMethodAsync > d__1。這是編譯器生成的狀態(tài)機結(jié)構(gòu)的盒子。1000個方法掛起,1000個盒子出現(xiàn)。

QueueSegment / IThreadPoolWorkItem[]。有幾千個這樣的方法,從技術上講,它們與具體的異步方法無關,而是與線程池中的隊列工作有關。在 .NET 框架中,線程池的隊列是一個非循環(huán)段的鏈表。這些段不會被重用;對于長度為 N 的段,一旦 N 個工作項被加入到該段的隊列中并從該段中退出,該段就會被丟棄并當作垃圾回收。

這就是 .NET Framework。這是 .NET Core:

對于 .NET Framework 上的這個示例,有超過500萬次分配,總共分配了大約145MB的內(nèi)存。對于 .NET Core 上的相同示例,只有大約1000個內(nèi)存分配,總共只有大約109KB。為什么這么少?

ExecutionContext。在 .NET Core 中,ExecutionContext 現(xiàn)在是不可變的。這樣做的缺點是,對上下文的每次更改,例如將值設置為 AsyncLocal<T>,都需要分配一個新的 ExecutionContext。然而,好處是,流動的上下文比改變它更常見,而且由于 ExecutionContext 現(xiàn)在是不可變的,我們不再需要作為流動的一部分進行克隆。“捕獲”上下文實際上就是從字段中讀取它,而不是讀取它并復制其內(nèi)容。因此,流動不僅比變化更常見,而且更便宜。

LogicalCallContext。這在 .NET Core 中已經(jīng)不存在了。在 .NET Core 中,ExecutionContext 唯一存在的東西是 AsyncLocal<T> 的存儲。其他在 ExecutionContext 中有自己特殊位置的東西都是以 AsyncLocal<T> 為模型的。例如,在 .NET Framework 中,模擬將作為 SecurityContext 的一部分流動,而SecurityContext 是 ExecutionContext 的一部分;在 .NET Core 中,模擬通過 AsyncLocal<SafeAccessTokenHandle> 流動,它使用 valueChangedHandler 來對當前線程進行適當?shù)母摹?/p>

QueueSegment / IThreadPoolWorkItem[]。在 .NET Core 中,ThreadPool 的全局隊列現(xiàn)在被實現(xiàn)為 ConcurrentQueue<T>,而 ConcurrentQueue<T> 已經(jīng)被重寫為一個由非固定大小的循環(huán)段組成的鏈表。一旦段的長度大到永遠不會被填滿因為穩(wěn)態(tài)的出隊列能夠跟上穩(wěn)態(tài)的入隊列,就不需要再分配額外的段,相同的足夠大的段就會被無休止地使用。

那么其他的分配呢,比如 Action、MoveNextRunner 和 <SomeMethodAsync>d__1? 要理解剩余的分配是如何被移除的,需要深入了解它在 .NET Core 上是如何工作的。

讓我們回到討論掛起時發(fā)生的事情:

if (!awaiter.IsCompleted) // we need to suspend when IsCompleted is false
{
    <>1__state = 1;
    <>u__2 = awaiter;
    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
    return;
}

不管目標是哪個平臺,這里發(fā)出的代碼都是相同的,所以不管是 .NET Framework 還是,為這個掛起生成的 IL 都是相同的。但是,改變的是 AwaitUnsafeOnCompleted 方法的實現(xiàn),在 .NET Core 中有很大的不同:

  • 1.事情的開始是一樣的:該方法調(diào)用 ExecutionContext.Capture() 來獲取當前執(zhí)行上下文。
  • 2.然后,事情偏離了 .NET Framework。.NET Core 中的 builder 只有一個字段:
public struct AsyncTaskMethodBuilder
{
    private Task<VoidTaskResult>? m_task;
    ...
}

在捕獲 ExecutionContext 之后,它檢查 m_task 字段是否包含一個 AsyncStateMachineBox<TStateMachine> 的實例,其中 TStateMachine 是編譯器生成的狀態(tài)機結(jié)構(gòu)體的類型。AsyncStateMachineBox<TStateMachine> 類型定義如下:

private class AsyncStateMachineBox<TStateMachine> :
    Task<TResult>, IAsyncStateMachineBox
    where TStateMachine : IAsyncStateMachine
{
    private Action? _moveNextAction;
    public TStateMachine? StateMachine;
    public ExecutionContext? Context;
    ...
}

與其說這是一個單獨的 Task,不如說這是一個任務(注意其基本類型)。該結(jié)構(gòu)并沒有對狀態(tài)機進行裝箱,而是作為該任務的強類型字段存在。我們不需要用單獨的 MoveNextRunner 來存儲 Action 和 ExecutionContext,它們只是這個類型的字段,而且由于這是存儲在構(gòu)建器的 m_task 字段中的實例,我們可以直接訪問它,不需要在每次暫停時重新分配。如果 ExecutionContext 發(fā)生變化,我們可以用新的上下文覆蓋該字段,而不需要分配其他東西;我們的任何 Action 仍然指向正確的地方。所以,在捕獲了 ExecutionContext 之后,如果我們已經(jīng)有了這個 AsyncStateMachineBox<TStateMachine> 的實例,這就不是這個方法第一次掛起了,我們可以直接把新捕獲的 ExecutionContext 存儲到其中。如果我們還沒有一個AsyncStateMachineBox<TStateMachine> 的實例,那么我們需要分配它:

var box = new AsyncStateMachineBox<TStateMachine>();
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine!
box.StateMachine = stateMachine;
box.Context = currentContext;

請注意源注釋為“重要”的那一行。這取代了 .NET Framework 中復雜的 SetStateMachine,使得 SetStateMachine 在 .NET Core 中根本沒有使用。你看到的 taskField 有一個指向 AsyncTaskMethodBuilder 的 m_task 字段的 ref。我們分配 AsyncStateMachineBox< tstatemachinebox >,然后通過 taskField 將對象存儲到構(gòu)建器的 m_task 中(這是在棧上的狀態(tài)機結(jié)構(gòu)中的構(gòu)建器),然后將基于堆棧的狀態(tài)機(現(xiàn)在已經(jīng)包含對盒子的引用)復制到基于堆的 AsyncStateMachineBox< tstatemachinebox > 中,這樣 AsyncStateMachineBox<TStateMachine> 適當?shù)夭⑦f歸地結(jié)束引用自己。這仍然是令人費解的,但卻是一種更有效的費解。

  • 3.然后,我們可以對這個 Action 上的一個方法進行操作,該方法將調(diào)用其 MoveNext 方法,該方法將在調(diào)用 StateMachine 的 MoveNext 之前執(zhí)行適當?shù)?ExecutionContext 恢復。該 Action 可以緩存到 _moveNextAction 字段中,以便任何后續(xù)使用都可以重用相同的 Action。然后,該 Action 被傳遞給 awaiter 的 UnsafeOnCompleted 來連接 continuation。

它解釋了為什么剩下的大部分分配都沒有了:<SomeMethodAsync>d__1 沒有被裝箱,而是作為任務本身的一個字段存在,MoveNextRunner 不再需要,因為它的存在只是為了存儲 Action 和 ExecutionContext。但是,根據(jù)這個解釋,我們?nèi)匀粦摽吹?000個操作分配,每個方法調(diào)用一個,但我們沒有。為什么?還有那些 QueueUserWorkItemCallback 對象呢?我們?nèi)匀辉?Task.Yield() 中進行排隊,為什么它們沒有出現(xiàn)呢?

正如我所提到的,將實現(xiàn)細節(jié)推入核心庫的好處之一是,它可以隨著時間的推移改進實現(xiàn),我們已經(jīng)看到了它是如何從 .NET Framework 發(fā)展到 .NET Core 的。它在最初為 .NET Core 重寫的基礎上進一步發(fā)展,增加了額外的優(yōu)化,這得益于對系統(tǒng)關鍵組件的內(nèi)部訪問。特別是,異步基礎設施知道 Task 和 TaskAwaiter 等核心類型。而且因為它知道它們并具有內(nèi)部訪問權(quán)限,所以它不必遵循公開定義的規(guī)則。C# 語言遵循的 awaiter 模式要求 awaiter 具有 AwaitOnCompleted 或 AwaitUnsafeOnCompleted 方法,這兩個方法都將 continuation 作為一個操作,這意味著基礎結(jié)構(gòu)需要能夠創(chuàng)建一個操作來表示 continuation,以便與基礎結(jié)構(gòu)不知道的任意 awaiter 一起工作。但是,如果基礎設施遇到它知道的 awaiter,它沒有義務采取相同的代碼路徑。對于 System.Private 中定義的所有核心 awaiter。因此,CoreLib 的基礎設施可以遵循更簡潔的路徑,完全不需要操作。這些 awaiter 都知道 IAsyncStateMachineBoxes,并且能夠?qū)?box 對象本身作為 continuation。例如,Task 返回的 YieldAwaitable.Yield 能夠?qū)?IAsyncStateMachineBox 本身作為工作項直接放入 ThreadPool 中,而等待任務時使用的 TaskAwaiter 能夠?qū)?IAsyncStateMachineBox 本身直接存儲到任務的延續(xù)列表中。不需要操作,也不需要 QueueUserWorkItemCallback。

因此,在非常常見的情況下,async 方法只等待 System.Private.CoreLib (Task, Task<TResult>, ValueTask, ValueTask<TResult>,YieldAwaitable,以及它們的ConfigureAwait 變體),最壞的情況下,只有一次開銷分配與 async 方法的整個生命周期相關:如果這個方法掛起了,它會分配一個單一的 Task-derived 類型來存儲所有其他需要的狀態(tài),如果這個方法從來沒有掛起,就不會產(chǎn)生額外的分配。

如果愿意,我們也可以去掉最后一個分配,至少以平攤的方式。如所示,有一個默認構(gòu)建器與 Task(AsyncTaskMethodBuilder) 相關聯(lián),類似地,有一個默認構(gòu)建器與任務 <TResult> (AsyncTaskMethodBuilder<TResult>) 和 ValueTask 和ValueTask<TResult> (AsyncValueTaskMethodBuilder 和 AsyncValueTaskMethodBuilder<TResult>,分別)相關聯(lián)。對于 ValueTask/ValueTask<TResult>,構(gòu)造器實際上相當簡單,因為它們本身只處理同步且成功完成的情況,在這種情況下,異步方法完成而不掛起,構(gòu)建器可以只返回一個 ValueTask.Completed 或者一個包含結(jié)果值的 ValueTask<TResult>。對于其他所有事情,它們只是委托給 AsyncTaskMethodBuilder/AsyncTaskMethodBuilder<TResult>,因為 ValueTask/ValueTask<TResult> 會被返回包裝一個 Task,它可以共享所有相同的邏輯。但是 .NET 6 and C# 10 引入了一個方法可以覆蓋逐個方法使用的構(gòu)建器的能力,并為 ValueTask/ValueTask<TResult> 引入了幾個專門的構(gòu)建器,它們能夠池化 IValueTaskSource/IValueTaskSource<TResult> 對象來表示最終的完成,而不是使用 Tasks。

我們可以在我們的樣本中看到這一點的影響。稍微調(diào)整一下之前分析的 SomeMethodAsync 函數(shù),讓它返回 ValueTask 而不是 Task:

static async ValueTask SomeMethodAsync()
{
    for (int i = 0; i < 1000; i++)
    {
        await Task.Yield();
    }
}

這將生成以下入口點:

[AsyncStateMachine(typeof(<SomeMethodAsync>d__1))]
private static ValueTask SomeMethodAsync()
{
    <SomeMethodAsync>d__1 stateMachine = default;
    stateMachine.<>t__builder = AsyncValueTaskMethodBuilder.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

現(xiàn)在,我們添加 [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] 到 SomeMethodAsync 的聲明中:

[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
static async ValueTask SomeMethodAsync()
{
    for (int i = 0; i < 1000; i++)
    {
        await Task.Yield();
    }
}

編譯器輸出如下:

[AsyncStateMachine(typeof(<SomeMethodAsync>d__1))]
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
private static ValueTask SomeMethodAsync()
{
    <SomeMethodAsync>d__1 stateMachine = default;
    stateMachine.<>t__builder = PoolingAsyncValueTaskMethodBuilder.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

整個實現(xiàn)的實際 C# 代碼生成,包括整個狀態(tài)機(沒有顯示),幾乎是相同的;唯一的區(qū)別是創(chuàng)建和存儲的構(gòu)建器的類型,因此在我們之前看到的任何引用構(gòu)建器的地方都可以使用。如果你看一下 PoolingAsyncValueTaskMethodBuilder 的代碼,你會看到它的結(jié)構(gòu)幾乎與 AsyncTaskMethodBuilder 相同,包括使用一些完全相同的共享例程來做一些事情,如特殊套管已知的 awaiter 類型。

關鍵的區(qū)別是,當方法第一次掛起時,它不是執(zhí)行新的 AsyncStateMachineBox<TStateMachine>(),而是執(zhí)行 StateMachineBox<TStateMachine>. rentfromcache(),并且在 async 方法 (SomeMethodAsync) 完成并等待返回的 ValueTask 完成時,租用的盒子會被返回到緩存中。這意味著(平攤)零分配:

這個緩存本身有點意思。對象池可能是一個好主意,也可能是一個壞主意。創(chuàng)建一個對象的成本越高,共享它們的價值就越大;因此,例如,對非常大的數(shù)組進行池化比對非常小的數(shù)組進行池化更有價值,因為更大的數(shù)組不僅需要更多的 CPU 周期和內(nèi)存訪問為零,它們還會給垃圾收集器帶來更大的壓力,使其更頻繁地收集垃圾。然而,對于非常小的對象,將它們池化可能會帶來負面影響。池只是內(nèi)存分配器,GC 也是,所以當您使用池時,您是在權(quán)衡與一個分配器相關的成本與另一個分配器相關的成本,并且 GC 在處理大量微小的、生存期短的對象方面非常高效。如果你在對象的構(gòu)造函數(shù)中做了很多工作,避免這些工作可以使分配器本身的開銷相形見絀,從而使池變得有價值。但是,如果您在對象的構(gòu)造函數(shù)中幾乎沒有做任何工作,并且將其進行池化,則您將打賭您的分配器(您的池)就所采用的訪問模式而言比 GC 更有效,而這通常是一個糟糕的賭注。還涉及其他成本,在某些情況下,您可能最終會有效地對抗 GC 的啟發(fā)式方法;例如,垃圾回收是基于一個前提進行優(yōu)化的,即從較高代(如gen2)對象到較低代(如gen0)對象的引用相對較少,但池化對象可以使這些前提失效。

我們今天為大家介紹了 C# 迭代器和 async/await under the covers,下期文章,我們將繼續(xù)介紹 SynchronizationContext 和 ConfigureAwait,更多關于C# async await迭代器的資料請關注腳本之家其它相關文章!

相關文章

  • C#將異步改成同步方法示例

    C#將異步改成同步方法示例

    這篇文章主要為大家介紹了C#將異步改成同步方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • Unity3D更改默認的腳本編輯器

    Unity3D更改默認的腳本編輯器

    這篇文章簡要的說明了如何去修改Unity默認的腳本編輯器,大大提升了靈活性和便捷性,文本有詳細的圖文介紹,能讓你觀看的更加清晰,希望對你有所幫助
    2021-06-06
  • C#從實體對象集合中導出Excel的代碼

    C#從實體對象集合中導出Excel的代碼

    數(shù)據(jù)的導出是項目中經(jīng)常要實現(xiàn)的功能,就拿最常見的要導出成Excel來說,網(wǎng)上看來看去,都是介紹從Datatable中導出
    2008-08-08
  • winform使用委托和事件來完成兩個窗體之間通信的實例

    winform使用委托和事件來完成兩個窗體之間通信的實例

    這篇文章介紹了winform使用委托和事件來完成兩個窗體之間通信的實例,有需要的朋友可以參考一下
    2013-09-09
  • CefSharp如何進行頁面的縮放(Ctrl+滾輪)

    CefSharp如何進行頁面的縮放(Ctrl+滾輪)

    CefSharp簡單來說就是一款.Net編寫的瀏覽器包,本文主要介紹了CefSharp如何進行頁面的縮放,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-06-06
  • Unity3D實現(xiàn)甜品消消樂游戲

    Unity3D實現(xiàn)甜品消消樂游戲

    這篇文章主要介紹了通過C# Unity3D繪制一個甜品消消樂游戲,文中的示例代碼講解詳細,對我們學習或工作有一定的幫助,感興趣的小伙伴可以學習一下
    2021-12-12
  • C#實現(xiàn)字體旋轉(zhuǎn)的方法

    C#實現(xiàn)字體旋轉(zhuǎn)的方法

    這篇文章主要介紹了C#實現(xiàn)字體旋轉(zhuǎn)的方法,涉及C#通過Matrix實現(xiàn)字體旋轉(zhuǎn)效果的方法,需要的朋友可以參考下
    2015-06-06
  • 登錄驗證全局控制的幾種方式總結(jié)(session)

    登錄驗證全局控制的幾種方式總結(jié)(session)

    在登陸驗證或者其他需要用到session全局變量的時候,歸結(jié)起來,主要有以下三種較方便的實現(xiàn)方式。(其中個人較喜歡使用第一種實現(xiàn)方法)
    2014-01-01
  • C#實現(xiàn)把圖片轉(zhuǎn)換成二進制以及把二進制轉(zhuǎn)換成圖片的方法示例

    C#實現(xiàn)把圖片轉(zhuǎn)換成二進制以及把二進制轉(zhuǎn)換成圖片的方法示例

    這篇文章主要介紹了C#實現(xiàn)把圖片轉(zhuǎn)換成二進制以及把二進制轉(zhuǎn)換成圖片的方法,結(jié)合具體實例形式分析了基于C#的圖片與二進制相互轉(zhuǎn)換以及圖片保存到數(shù)據(jù)庫的相關操作技巧,需要的朋友可以參考下
    2017-06-06
  • C#設計模式實現(xiàn)之迭代器模式

    C#設計模式實現(xiàn)之迭代器模式

    迭代器模式把對象的職責分離,職責分離可以最大限度減少彼此之間的耦合程度,從而建立一個松耦合的對象,這篇文章主要給大家介紹了關于C#設計模式實現(xiàn)之迭代器模式的相關資料,需要的朋友可以參考下
    2021-08-08

最新評論