.NET 6.0 中的 await 原理淺析

来源:https://www.cnblogs.com/broadm/archive/2023/11/15/17833442.html
-Advertisement-
Play Games

前言 看過不少關於 await 的原理的文章,也知道背後是編譯器給轉成了狀態機實現的,但是具體是怎麼完成的,回調又是如何銜接的,一直都沒有搞清楚,這次下定決心把源碼自己跑了下,終於豁然開朗了 本文的演示代碼基於 VS2022 + .NET 6 示例 public class Program { st ...


前言

看過不少關於 await 的原理的文章,也知道背後是編譯器給轉成了狀態機實現的,但是具體是怎麼完成的,回調又是如何銜接的,一直都沒有搞清楚,這次下定決心把源碼自己跑了下,終於豁然開朗了

本文的演示代碼基於 VS2022 + .NET 6

示例

public class Program
{
    static int Work()
    {
        Console.WriteLine("In Task.Run");
        return 1;
    }

    static async Task TestAsync()
    {
        Console.WriteLine("Before Task.Run");
        await Task.Run(Work);
        Console.WriteLine("After Task.Run");
    }

    static void Main()
    {
        _ = TestAsync();
        Console.WriteLine("End");
        Console.ReadKey();
    }
}
  • 很簡單的非同步代碼,我們來看下,編譯器把它變成了啥
class Program
{
    static int Work()
    {
        Console.WriteLine("In Task.Run");
        return 1;
    }

    static Task TestAsync()
    {
        var stateMachine = new StateMachine()
        {
            _builder = AsyncTaskMethodBuilder.Create(),
            _state = -1
        };
        stateMachine._builder.Start(ref stateMachine);
        return stateMachine._builder.Task;
    }

    static void Main()
    {
        _ = TestAsync();
        Console.WriteLine("End");
        Console.ReadKey();
    }

    class StateMachine : IAsyncStateMachine
    {
        public int _state;
        public AsyncTaskMethodBuilder _builder;
        private TaskAwaiter<int> _awaiter;

        void IAsyncStateMachine.MoveNext()
        {
            int num = _state;
            try
            {
                TaskAwaiter<int> awaiter;
                if (num != 0)
                {
                    Console.WriteLine("Before Task.Run");
                    awaiter = Task.Run(Work).GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        _state = 0;
                        _awaiter = awaiter;
                        StateMachine stateMachine = this;
                        _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    awaiter = _awaiter;
                    _awaiter = default;
                    _state = -1;
                }
                awaiter.GetResult();
                Console.WriteLine("After Task.Run");
            }
            catch (Exception exception)
            {
                _state = -2;
                _builder.SetException(exception);
                return;
            }
            _state = -2;
            _builder.SetResult();
        }

        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { }
    }
}
  • 編譯後的代碼經過我的整理,命名簡化了,更容易理解

狀態機實現

  • 我們看到實際是生成了一個隱藏的狀態機類 StateMachine

  • 把狀態機的初始狀態 _state 設置 -1

  • stateMachine._builder.Start(ref stateMachine); 啟動狀態機,內部實際調用的就是狀態機的 MoveNext 方法

  • Task.Run 創建一個任務, 把委托放在 Task.m_action 欄位,丟到線程池,等待調度

  • 任務線上程池內被調度完成後,是怎麼回到這個狀態機繼續執行後續代碼的呢?
    _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 就是關鍵了, 跟下去,到瞭如下的代碼:

    if (!this.AddTaskContinuation(stateMachineBox, false))
    {
        ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, true);
    }
    bool AddTaskContinuation(object tc, bool addBeforeOthers)
    {
        return !this.IsCompleted && ((this.m_continuationObject == null && Interlocked.CompareExchange(ref this.m_continuationObject, tc, null) == null) || this.AddTaskContinuationComplex(tc, addBeforeOthers));
    }
    
    • 這裡很清楚的看到,嘗試把狀態機對象(實際是狀態機的包裝類),賦值到 Task.m_continuationObject, 如果操作失敗,則把狀態機對象丟進線程池等待調度,這裡為什麼這麼實現,看一下線程池是怎麼執行的就清楚了

線程池實現

  • .NET6 的線程池實現,實際是放到了 PortableThreadPool, 具體調試步驟我就不放了,直接說結果就是, 線程池線程從任務隊列中拿到任務後都執行了 DispatchWorkItem 方法
static void DispatchWorkItem(object workItem, Thread currentThread)
{
    Task task = workItem as Task;
    if (task != null)
    {
        task.ExecuteFromThreadPool(currentThread);
        return;
    }
    Unsafe.As<IThreadPoolWorkItem>(workItem).Execute();
}
virtual void ExecuteFromThreadPool(Thread threadPoolThread)
{
    this.ExecuteEntryUnsafe(threadPoolThread);
}
  • 我們看到, 線程池隊列中的任務都是 object 類型的, 這裡進行了類型判斷, 如果是 Task , 直接執行 task.ExecuteFromThreadPool, 更有意思的這個方法是個虛方法,後面說明

  • ExecuteFromThreadPool 繼續追下去,我們來到了這裡,代碼做了簡化

    private void ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread = null)
    {
        this.InnerInvoke();
        this.Finish(true);
    }
    
    virtual void InnerInvoke()
    {
        Action action = this.m_action as Action;
        if (action != null)
        {
            action();
            return;
        }
    }
    
  • 很明顯 this.InnerInvoke 就是執行了最開始 Task.Run(Work) 封裝的委托了, 在 m_action 欄位

  • this.Finish(true); 跟下去會發現會調用 FinishStageTwo 設置任務的完成狀態,異常等, 繼續調用 FinishStageThree 就來了重點: FinishContinuations 這個方法就是銜接後續回調的核心

    internal void FinishContinuations()
    {
        object obj = Interlocked.Exchange(ref this.m_continuationObject, Task.s_taskCompletionSentinel);
        if (obj != null)
        {
            this.RunContinuations(obj);
        }
    }
    
  • 還記得狀態機實現麽, Task.m_continuationObject 欄位實際存儲的就是狀態機的包裝類,這裡線程池線程也會判斷這個欄位有值的話,就直接使用它執行後續代碼了

    void RunContinuations(object continuationObject)
    {
        var asyncStateMachineBox = continuationObject as IAsyncStateMachineBox;
        if (asyncStateMachineBox != null)
        {
            AwaitTaskContinuation.RunOrScheduleAction(asyncStateMachineBox, flag2);
            return;
        }
    }
    
    static void RunOrScheduleAction(IAsyncStateMachineBox box, bool allowInlining)
    {
        if (allowInlining && AwaitTaskContinuation.IsValidLocationForInlining)
        {
            box.MoveNext();
            return;
        }
    }
    

總結

  1. Task.Run 創建 Task, 把委托放在 m_action 欄位, 把 Task 壓入線程池隊列,等待調度
  2. _builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 嘗試把狀態機對象放在 Task.m_continuationObject 欄位上,等待線程池線程調度完成任務後使用(用來執行後續),若操作失敗,直接把狀態機對象壓入線程池隊列,等待調度
  3. 線程池線程調度任務完成後,會判斷 Task.m_continuationObject 有值,直接執行它的 MoveNext

備註

  1. 狀態機實現中,嘗試修改 Task.m_continuationObject,可能會失敗,
    就會直接把狀態機對象壓入線程池, 但是線程池調度,不都是判斷是不是 Task 類型麽, 其實狀態機的包裝類是 Task 的子類,哈哈,是不是明白了

    class AsyncStateMachineBox<TStateMachine> : Task<TResult>, IAsyncStateMachineBox where TStateMachine : IAsyncStateMachine
    
    static void DispatchWorkItem(object workItem, Thread currentThread)
    {
        Task task = workItem as Task;
        if (task != null)
        {
            task.ExecuteFromThreadPool(currentThread);
            return;
        }
        Unsafe.As<IThreadPoolWorkItem>(workItem).Execute();
    }
    
  • 還有就是狀態機包裝類,重寫了 Task.ExecuteFromThreadPool,所以線程池調用 task.ExecuteFromThreadPool 就是直接調用了狀態機的 MoveNext 了, Soga ^_^
    override void ExecuteFromThreadPool(Thread threadPoolThread)
    {
        this.MoveNext(threadPoolThread);
    }
    
參考鏈接
  • 關於線程池和非同步的更深刻的原理,大家可以參考下麵的文章

概述 .NET 6 ThreadPool 實現: https://www.cnblogs.com/eventhorizon/p/15316955.html

.NET Task 揭秘(2):Task 的回調執行與 await: https://www.cnblogs.com/eventhorizon/p/15912383.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 關於Node.js的開發者來說,在開發機器上管理多個不同版本的Node.js是一個常見痛點。之前在開發者安全大全專欄中,提到過解決方法:使用nvm,如果對於nvm還不瞭解的話,可以前往瞭解。 對於TJ來說,因為習慣敲命令了,所以nvm其實已經夠用了。但是,有的小伙伴還是更喜歡可視化的管理工具。所以, ...
  • 我希望在職業生涯早期就開始做的事情和我希望以不同的方式做的事情。 大家好,我已經做了八年半的軟體工程師。這篇文章來源於我最近對自己在職業生涯中希望早點開始做的事情以及希望以不同方式做的事情的自我反思。 我在這裡分享的對任何希望提高和進步到高級甚至更高職位的初級至中級開發者都很有用。 0 大綱 我的職 ...
  • 資料庫事務是什麼?事務的四大特性是什麼? 1.資料庫事務 事務是一組原子性的 SQL 語句,或者說一個獨立的工作單元。如果資料庫引擎能夠成功地對資料庫應用該組操作的全部語句,那麼就執行該組查詢。如果其中任何一條語句因為崩潰或其他原因無法執行,那麼所有的語句都不會執行。也就是說,事務內的語句,要麼全部 ...
  • 學習視頻:【孫哥說Spring5:從設計模式到基本應用到應用級底層分析,一次深入淺出的Spring全探索。學不會Spring?只因你未遇見孫哥】 第四章、註入(Injection) 1.什麼是註入 通過Spring工廠及配置文件,為所創建對象的成員變數賦值 1.1為什麼需要註入 “通過編碼的方式,為 ...
  • 公眾號「架構成長指南」,專註於生產實踐、雲原生、分散式系統、大數據技術分享。 在本文中,我們將討論一些重要且常見的 Java Lambda 表達式面試問題和解答 1.什麼是 Lambda 表達式? lambda表達式只是一個沒有任何名稱的函數,它甚至可以用作函數中的參數,Lambda 表達式有利於函 ...
  • 目錄abstract class 和 interface 有什麼區別1.抽象類1.1抽象類的格式1.2抽象類註意事項2.介面2.1介面的格式2.2介面可以多繼承2.3介面的實現(implements)3.異同 abstract class 和 interface 有什麼區別 1.抽象類 抽象類:聲明 ...
  • 日誌在程式中的重要性非常的重要,當系統發生故障時,我們要隨時能排查出相關的日誌,細數日誌在Rust中的定義依賴及其實現。 ...
  • 本文討論了一次故障排查的過程,通過監控工具和分析信息,但最終沒有找到根本原因。文章強調了故障排查的複雜性和重要性,提醒保持冷靜和耐心,在團隊合作和知識共用的基礎上解決問題,並總結了從這次故障中學到的經驗和教訓。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...