探索c#之Async、Await剖析

蘑菇先生發表於2015-06-15

閱讀目錄:

  1. 基本介紹
  2. 基本原理剖析
  3. 內部實現剖析
  4. 重點注意的地方
  5. 總結

基本介紹

Async、Await是net4.x新增的非同步程式設計方式,其目的是為了簡化非同步程式編寫,和之前APM方式簡單對比如下。

APM方式,BeginGetRequestStream需要傳入回撥函式,執行緒碰到BeginXXX時會以非阻塞形式繼續執行下面邏輯,完成後回撥先前傳入的函式。

    HttpWebRequest myReq =(HttpWebRequest)WebRequest.Create("http://cnblogs.com/");
     myReq.BeginGetRequestStream();
     //to do

Async方式,使用Async標記Async1為非同步方法,用Await標記GetRequestStreamAsync表示方法內需要耗時的操作。主執行緒碰到await時會立即返回,繼續以非阻塞形式執行主執行緒下面的邏輯。當await耗時操作完成時,繼續執行Async1下面的邏輯

static async void Async1()
    {
        HttpWebRequest myReq = (HttpWebRequest)WebRequest.Create("http://cnblogs.com/");
        await myReq.GetRequestStreamAsync();
        //to do
    }     

上面是net類庫實現的非同步,如果要實現自己方法非同步。
APM方式:

        public delegate int MyDelegate(int x);   
        MyDelegate mathDel = new MyDelegate((a) => { return 1; });
         mathDel.BeginInvoke(1, (a) => { },null);

Async方式:

static async void Async2()
    {
        await Task.Run(() => { Thread.Sleep(500); Console.WriteLine("bbb"); });
        Console.WriteLine("ccc");
    }
  Async2();
  Console.WriteLine("aaa");

對比下來發現,async/await是非常簡潔優美的,需要寫的程式碼量更少,更符合人們編寫習慣。
因為人的思維對線性步驟比較好理解的。

APM非同步回撥的執行步驟是:A邏輯->假C回撥邏輯->B邏輯->真C回撥邏輯,這會在一定程度造成思維的混亂,當一個專案中出現大量的非同步回撥時,就會變的難以維護。
Async、Await的加入讓原先這種混亂的步驟,重新撥正了,執行步驟是:A邏輯->B邏輯->C邏輯。

基本原理剖析

作為一個程式設計師的自我修養,刨根問底的好奇心是非常重要的。 Async剛出來時會讓人有一頭霧水的感覺,await怎麼就直接返回了,微軟怎麼又出一套新的非同步模型。那是因為習慣了之前的APM非線性方式導致的,現在重歸線性步驟反而不好理解。 學習Async時候,可以利用已有的APM方式去理解,以下程式碼純屬虛構
比如把Async2方法想象APM方式的Async3方法:

static async void Async3()
    {
        var task= await Task.Run(() => { Thread.Sleep(500); Console.WriteLine("bbb"); });
       //註冊task完成後回撥
        task.RegisterCompletedCallBack(() =>
        {
            Console.WriteLine("ccc");
        });
    }

上面看其來就比較好理解些的,再把Async3方法想象Async4方法:

static  void Async4()
    {
        var thread = new Thread(() =>
         {
             Thread.Sleep(500);
             Console.WriteLine("bbb");
         });
        //註冊thread完成後回撥
        thread.RegisterCompletedCallBack(() =>
        {
            Console.WriteLine("ccc");
        });
        thread.Start();
    }

這樣看起來就非常簡單明瞭,連async都去掉了,變成之前熟悉的程式設計習慣。雖然程式碼純屬虛構,但基本思想是相通的,差別在於實現細節上面。

內部實現剖析

作為一個程式設計師的自我修養,嚴謹更是不可少的態度。上面的基本思想雖然好理解了,但具體細節呢,程式設計是個來不得半點虛假的工作,那虛構的程式碼完全對不住看官們啊。

繼續看Async2方法,反編譯後的完整程式碼如下:

internal class Program
{
    // Methods
    [AsyncStateMachine(typeof(<Async2>d__2)), DebuggerStepThrough]
    private static void Async2()
    {
        <Async2>d__2 d__;
        d__.<>t__builder = AsyncVoidMethodBuilder.Create();
        d__.<>1__state = -1;
        d__.<>t__builder.Start<<Async2>d__2>(ref d__);
    }

    private static void Main(string[] args)
    {
        Async2();
        Console.WriteLine("aaa");
        Console.ReadLine();
    }

    // Nested Types
    [CompilerGenerated]
    private struct <Async2>d__2 : IAsyncStateMachine
    {
        // Fields
        public int <>1__state;
        public AsyncVoidMethodBuilder <>t__builder;
        private object <>t__stack;
        private TaskAwaiter <>u__$awaiter3;

        // Methods
        private void MoveNext()
        {
            try
            {
                TaskAwaiter awaiter;
                bool flag = true;
                switch (this.<>1__state)
                {
                    case -3:
                        goto Label_00C5;

                    case 0:
                        break;

                    default:
                        if (Program.CS$<>9__CachedAnonymousMethodDelegate1 == null)
                        {
                            Program.CS$<>9__CachedAnonymousMethodDelegate1 = new Action(Program.<Async2>b__0);
                        }
                        awaiter = Task.Run(Program.CS$<>9__CachedAnonymousMethodDelegate1).GetAwaiter();
                        if (awaiter.IsCompleted)
                        {
                            goto Label_0090;
                        }
                        this.<>1__state = 0;
                        this.<>u__$awaiter3 = awaiter;
                        this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Async2>d__2>(ref awaiter, ref this);
                        flag = false;
                        return;
                }
                awaiter = this.<>u__$awaiter3;
                this.<>u__$awaiter3 = new TaskAwaiter();
                this.<>1__state = -1;
            Label_0090:
                awaiter.GetResult();
                awaiter = new TaskAwaiter();
                Console.WriteLine("ccc");
            }
            catch (Exception exception)
            {
                this.<>1__state = -2;
                this.<>t__builder.SetException(exception);
                return;
            }
        Label_00C5:
            this.<>1__state = -2;
            this.<>t__builder.SetResult();
        }

        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine param0)
        {
            this.<>t__builder.SetStateMachine(param0);
        }
    }

    public delegate int MyDelegate(int x);
}

 
Collapse Methods
 
View Code

發現async、await不見了,原來又是編譯器級別提供的語法糖優化,所以說async不算是全新的非同步模型。 可以理解為async更多的是線性執行步驟的一種迴歸,專門用來簡化非同步程式碼編寫。
從反編譯後的程式碼看出編譯器新生成一個繼承IAsyncStateMachine 的狀態機結構asyncd(程式碼中叫<Async2>d__2,後面簡寫AsyncD),下面是基於反編譯後的程式碼來分析的

IAsyncStateMachine最基本的狀態機介面定義:

public interface IAsyncStateMachine
{
    void MoveNext();
    void SetStateMachine(IAsyncStateMachine stateMachine);
}

既然沒有了async、await語法糖的阻礙,就可以把程式碼執行流程按線性順序來理解,其整個執行步驟如下:

1. 主執行緒呼叫Async2()方法
2. Async2()方法內初始化狀態機狀態為-1,啟動AsyncD
3. MoveNext方法內部開始執行,其task.run函式是把任務扔到執行緒池裡,返回個可等待的任務控制程式碼。MoveNext原始碼剖析:

//要執行任務的委託

 Program.CS$<>9__CachedAnonymousMethodDelegate1 = new Action(Program.<Async2>b__0);

//開始使用task做非同步,是net4.0基於任務task的程式設計方式。

 awaiter =Task.Run(Program.CS$<>9__CachedAnonymousMethodDelegate1).GetAwaiter();

//設定狀態為0,以便再次MoveNext直接break,執行switch後面的邏輯,典型的狀態機模式。

this.<>1__state = 0;

//返回撥用async2方法的執行緒,讓其繼續執行主執行緒後面的邏輯

this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Async2>d__2>(ref awaiter, ref this);
return;

4. 這時就已經有2個執行緒在跑了,分別是主執行緒和Task.Run在跑的任務執行緒。

5. 執行主執行緒後面邏輯輸出aaa,任務執行緒執行完成後輸出bbb、在繼續執行任務執行緒後面的業務邏輯輸出ccc。

Label_0090: 
awaiter.GetResult(); 
awaiter = new TaskAwaiter();
Console.WriteLine("ccc");

這裡可以理解為async把整個主執行緒同步邏輯,分拆成二塊。 第一塊是在主執行緒直接執行,第二塊是在任務執行緒完成後執行, 二塊中間是任務執行緒在跑,其原始碼中awaiter.GetResult()就是在等待任務執行緒完成後去執行第二塊。
從使用者角度來看執行步驟即為: 主執行緒A邏輯->非同步任務執行緒B邏輯->主執行緒C邏輯。

        Test();
        Console.WriteLine("A邏輯");
        static async void Test()
        {
            await Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("B邏輯"); });
            Console.WriteLine("C邏輯"); 
        }

回過頭來對比下基本原理剖析小節中的虛構方法Async4(),發現區別在於一個是完成後回撥,一個是等待完成後再執行,這也是實現非同步最基本的兩大類方式。

重點注意的地方

主執行緒A邏輯->非同步任務執行緒B邏輯->主執行緒C邏輯。

注意:這3個步驟是有可能會使用同一個執行緒的,也可能會使用2個,甚至3個執行緒。 可以用Thread.CurrentThread.ManagedThreadId測試下得知。

     Async7();
     Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
    static async void Async7()
    {
        await Task.Run(() =>
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
        });
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 
    }

正由於此,才會有言論說Async不用開執行緒,也有說需要開執行緒的,從單一方面來講都是對的,也都是錯的。 上面原始碼是從簡分析的,具體async內部會涉及到執行緒上下文切換,執行緒複用、排程等。 想深入的同學可以研究下ExecutionContextSwitcher、 SecurityContext.RestoreCurrentWI、ExecutionContext這幾個東東。

其實具體的物理執行緒細節可以不用太關心,知道其【主執行緒A邏輯->非同步任務執行緒B邏輯->主執行緒C邏輯】這個基本原理即可。 另外Async也會有執行緒開銷的,所以要合理分業務場景去使用。

總結

從逐漸剖析Async中發現,Net提供的非同步方式基本上一脈相承的,如:
1. net4.5的Async,拋去語法糖就是Net4.0的Task+狀態機。
2. net4.0的Task, 退化到3.5即是(Thread、ThreadPool)+實現的等待、取消等API操作。

本文以async為起點,簡單剖析了其內部原理及實現,希望對大家有所幫助。

 

相關文章