C# 完全搞懂async/await

前言

Talk is cheap, Show you the code first!html

private void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
    AsyncMethod();
    Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}

private async Task AsyncMethod()
{
    var ResultFromTimeConsumingMethod = TimeConsumingMethod();
    string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine(Result);
    //返回值是Task的函數能夠不用return
}

//這個函數就是一個耗時函數,多是IO操做,也多是cpu密集型工做。
private Task<string> TimeConsumingMethod()
{            
    var task = Task.Run(()=> {
        Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        return "Hello I am TimeConsumingMethod";
    });

    return task;
}

我靠,這麼複雜!!!居然有三個函數!!!居然有那麼多行!!!多線程

彆着急,慢慢看完,最後的時候你會發現使用async/await真的炒雞優雅。異步

異步方法的結構

上面是一個的使用async/await的例子(爲了方便解說原理我才寫的這樣複雜的)。
使用async/await能很是簡單的建立異步方法,防止耗時操做阻塞當前線程。
使用async/await來構建的異步方法,邏輯上主要有下面三個結構:async

調用異步方法

private void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
    AsyncMethod();//這個方法就是異步方法,異步方法的調用與通常方法徹底同樣
    Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}

注意:微軟建議異步方法的命名是在方法名後添加Aysnc後綴,示例是我爲了讀起來方便作成了前綴,在真正構建異步方法的時候請注意用後綴。(好吧我認可是我忘記了,而後圖片也都截好了再修改太麻煩了。。。。就懶得從新再修改了)ide

異步方法的返回類型只能是voidTaskTask<TResult>。示例中異步方法的返回值類型是Task函數

另外,上面的AsyncMethod()會被編譯器提示報警,以下圖:

由於是異步方法,因此編譯器提示在前面使用await關鍵字,這個後面再說,爲了避免引入太多概念致使難以理解暫時就先這麼放着。ui

異步方法本體

private async Task AsyncMethod()
{
    var ResultFromTimeConsumingMethod = TimeConsumingMethod();
    string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine(Result);
    //返回值是Task的函數能夠不用return
}

async來修飾一個方法,代表這個方法是異步的,聲明的方法的返回類型必須爲:voidTaskTask<TResult>。方法內部必須含有await修飾的方法,若是方法內部沒有await關鍵字修飾的表達式,哪怕函數被async修飾也只能算做同步方法,執行的時候也是同步執行的。spa

被await修飾的只能是Task或者Task<TResule>類型,一般狀況下是一個返回類型是Task/Task<TResult>的方法,固然也能夠修飾一個Task/Task<TResult>變量,await只能出如今已經用async關鍵字修飾的異步方法中。上面代碼中就是修飾了一個變量ResultFromTimeConsumingMethod線程

關於被修飾的對象,也就是返回值類型是TaskTask<TResult>函數或者Task/Task<TResult>類型的變量:若是是被修飾對象的前面用await修飾,那麼返回值其實是void或者TResult(示例中ResultFromTimeConsumingMethodTimeConsumingMethod()函數的返回值,也就是Task<string>類型,當ResultFromTimeConsumingMethod在前面加了await關鍵字後 await ResultFromTimeConsumingMethod實際上徹底等於 ResultFromTimeConsumingMethod.Result)。若是沒有await,返回值就是Task或者Task<TResult>翻譯

耗時函數

//這個函數就是一個耗時函數,多是IO密集型操做,也多是cpu密集型工做。
private Task<string> TimeConsumingMethod()
{            
    var task = Task.Run(()=> {
        Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        return "Hello I am TimeConsumingMethod";
    });

    return task;
}

這個函數纔是真正幹活的(爲了讓邏輯層級更分明,我把這部分專門作成了一個函數,在後面我會精簡一下直接放到異步函數中,畢竟活在哪都是幹)。

在示例中是一個CPU密集型的工做,我另開一線程讓他拼命幹活幹5s。若是是IO密集型工做好比文件讀寫等能夠直接調用.Net提供的類庫,對於這些類庫底層具體怎麼實現的?是用了多線程仍是DMA?或者是多線程+DMA?這些問題我沒有深究可是從表象看起來和我用Task另開一個線程去作耗時工做是同樣的。

await只能修飾Task/Task<TResult>類型,因此這個耗時函數的返回類型只能是Task/Task<TResult>類型。

總結:有了上面三個結構就能完成使用一次異步函數。

async/await異步函數的原理

在開始講解這兩個關鍵字以前,爲了方便,對某些方法作了一些拆解,拆解後的代碼塊用代號指定:

上圖對示例代碼作了一些指定具體就是:

  • Caller表明調用方函數,在上面的代碼中就是button1_Click函數。
  • CalleeAsync表明被調用函數,由於代碼中被調用函數是一個異步函數,按照微軟建議的命名添加了Async後綴,在上面示例代碼中就是AsyncMethod()函數。
  • CallerChild1表明調用方函數button1_Click在調用異步方法CalleeAsync以前的那部分代碼。
  • CallerChild2表明調用方函數button1_Click在調用異步方法CalleeAsync以後的那部分代碼。
  • CalleeChild1表明被調用方函數AsyncMethod遇到await關鍵字以前的那部分代碼。
  • CalleeChild2表明被調用方函數AsyncMethod遇到await關鍵字以後的那部分代碼。
  • TimeConsumingMethod是指被await修飾的那部分耗時代碼(實際上我代碼中也是用的這個名字來命名的函數)

示例代碼的執行流程


爲了方便觀看我模糊掉了對本示例沒有用的輸出。
這裏涉及到了兩個線程,線程ID分別是1和3。

Caller函數被調用,先執行CallerChild1代碼,這裏是同步執行與通常函數同樣,而後遇到了異步函數CalleeAsync。

在CalleeAsync函數中有await關鍵字,await的做用是打分裂點。

編譯器會把整個函數(CalleeAsync)從這裏分裂成兩個函數。await關鍵字以前的代碼做爲一個函數(按照我上面定義的指代,下文中就叫這部分代碼CalleeChild1)await關鍵字以後的代碼做爲一個函數(CalleeChild2)。

CalleeChild1在調用方線程執行(在示例中就是主線程Thread1),執行到await關鍵字以後,另開一個線程耗時工做在Thread3中執行,而後當即返回。這時調用方會繼續執行下面的代碼CallerChild2(注意是Caller不是Callee)。

在CallerChild2被執行期間,TimeConsumingMethod也在異步執行(多是在別的線程也多是CPU不參與操做直接DMA的IO操做)。

當TimeConsumingMethod執行結束後,CalleeChild2也就具有了執行條件,而這個時候CallerChild2可能執行完了也可能沒有,因爲CallerChild2與CalleeChild2都會在Caller的線程執行,這裏就會有衝突應該先執行誰,編譯器會在合適的時候在Caller的線程執行這部分代碼。示意圖以下:

請注意,CalleeChild2在上圖中並無畫任何箭頭,由於這部分代碼的執行是由編譯器決定的,暫時沒法具體描述是何時執行。

總結一下:

整個流程下來,除了TimeConsumingMethod函數是在Thread3中執行的,剩餘代碼都是在主線程Thread1中執行的.

也就是說異步方法運行在當前同步上下文中,只有激活的時候才佔用當前線程的時間,異步模型採用時間片輪轉來實現(這一點我沒考證,僅做參考)。

你也許會說,明明新加了一個Thread3線程怎麼能說是運行在當前的線程中呢?這裏說的異步方法運行在當前線程上的意思是由CalleeAsync分裂出來的CalleeChild1和CalleeChild2的確是運行在Thread1上的。

帶返回值的異步函數

以前的示例代碼中異步函數是沒有返回值的,做爲理解原理足夠了,可是在實際應用場景中,帶返回值的應用纔是最經常使用的。那麼,上代碼:

private void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
    var ResultTask  = AsyncMethod();
    Console.WriteLine(ResultTask.Result);
    Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
}

private async Task<string> AsyncMethod()
{
    var ResultFromTimeConsumingMethod = TimeConsumingMethod();
    string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine(Result);
    return Result;
}

//這個函數就是一個耗時函數,多是IO操做,也多是cpu密集型工做。
private Task<string> TimeConsumingMethod()
{            
    var task = Task.Run(()=> {
        Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        return "Hello I am TimeConsumingMethod";
    });

    return task;
}

主要更改的地方在這裏:


按理說沒錯吧?然而,這代碼一旦執行就會卡死。

死鎖

是的,死鎖。分析一下爲何:

按照以前我劃定的代碼塊指定,在添加了新代碼後CallerChild2與CalleeChild2的劃分如上圖。

這兩部分代碼塊都是在同一個線程上執行的,也就是主線程Thread1,並且一般狀況下CallerChild2是會早於CalleeChild2執行的(畢竟CalleeChild2得在耗時代碼塊執行以後執行)。

Console.WriteLine(ResultTask.Result);(CallerChild2)實際上是在請求CalleeChild2的執行結果,此時明顯CalleeChild2尚未結束沒有return任何結果,那Console.WriteLine(ResultTask.Result);就只能阻塞Thread1等待,直到CalleeChild2有結果。

然而問題就在這,CalleeChild2也是在Thread1上執行的,此時CallerChild2一直佔用Thread1等待CalleeChild2的結果,耗時程序結束後輪到CalleeChild2執行的時候CalleeChild2又因Thread1被CallerChild2佔用而搶不到線程,永遠沒法return,那麼CallerChild2就會永遠等下去,這就形成了死鎖。

解決辦法有兩種一個是把Console.WriteLine(ResultTask.Result);放到一個新開線程中等待(我的以爲這方法有點麻煩,畢竟要新開線程),還有一個方法是把Caller也作成異步方法:

ResultTask.Result變成了ResultTask 的緣由上面也說了,await修飾的Task/Task<TResult>獲得的是TResult。

之因此這樣就能解決問題是由於嵌套了兩個異步方法,如今的Caller也成了一個異步方法,當Caller執行到await後直接返回了(await拆分方法成兩部分),CalleeChild2執行以後才輪到Caller中await後面的代碼塊(Console.WriteLine(ResultTask.Result);)。

另外,把Caller作成異步的方法也解決了一開始的那個警告,還記得麼?

這樣沒省多少事啊?

到如今,你可能會說:使用async/await不比直接用Task.Run()來的簡單啊?好比我用TaskTaskContinueWith方法也能實現:

private void button1_Click(object sender, EventArgs e)
{
    var ResultTask = Task.Run(()=> {
        Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
        return "Hello I am TimeConsumingMethod";
    });

    ResultTask.ContinueWith(OnDoSomthingIsComplete);

}

private void OnDoSomthingIsComplete(Task<string> t)
{
    Action action = () => {
        textBox1.Text = t.Result;
    };
    textBox1.Invoke(action);
    Console.WriteLine("Continue Thread ID :" + Thread.CurrentThread.ManagedThreadId);
}

是的,上面的代碼也能實現。可是,async/await的優雅的打開方式是這樣的:

private async void button1_Click(object sender, EventArgs e)
{
    var t = Task.Run(() => {
        Thread.Sleep(5000);
        return "Hello I am TimeConsumingMethod";
    });
    textBox1.Text = await t;
}

看到沒,驚不驚喜,意不意外,寥寥幾行就搞定了,不用再多寫那麼多函數,使用起來也很靈活。最讓人頭疼的跨線程修改控件的問題完美解決了,不再用使用Invoke了,由於修改控件的操做壓根就是在原來的線程上作的,還能不阻塞UI。

參考:
死鎖問題 http://www.javashuo.com/article/p-gqbuqdfo-bx.html
該博主是翻譯的英文資料,英文原文:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
https://www.cnblogs.com/zhili/archive/2013/05/15/Csharp5asyncandawait.html
http://www.cnblogs.com/heyuquan/archive/2013/04/26/3045827.html
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

相關文章
相關標籤/搜索