C# 彻底搞懂async/await
前言
Talk is cheap, Show you the code first!
?
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來構建的異步方法,邏輯上主要有下面三個結構:
調用異步方法
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后綴,示例是我為了讀起來方便做成了前綴,在真正構建異步方法的時候請注意用后綴。(好吧我承認是我忘記了,然后圖片也都截好了再修改太麻煩了。。。。就懶得重新再修改了)
異步方法的返回類型只能是void、Task、Task<TResult>。示例中異步方法的返回值類型是Task。
另外,上面的AsyncMethod()會被編譯器提示報警,如下圖:
因為是異步方法,所以編譯器提示在前面使用await關鍵字,這個后面再說,為了不引入太多概念導致難以理解暫時就先這么放著。
異步方法本體
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來修飾一個方法,表明這個方法是異步的,聲明的方法的返回類型必須為:void或Task或Task<TResult>。方法內部必須含有await修飾的方法,如果方法內部沒有await關鍵字修飾的表達式,哪怕函數被async修飾也只能算作同步方法,執行的時候也是同步執行的。
被await修飾的只能是Task或者Task<TResule>類型,通常情況下是一個返回類型是Task/Task<TResult>的方法,當然也可以修飾一個Task/Task<TResult>變量,await只能出現在已經用async關鍵字修飾的異步方法中。上面代碼中就是修飾了一個變量ResultFromTimeConsumingMethod。
關于被修飾的對象,也就是返回值類型是Task和Task<TResult>函數或者Task/Task<TResult>類型的變量:如果是被修飾對象的前面用await修飾,那么返回值實際上是void或者TResult(示例中ResultFromTimeConsumingMethod是TimeConsumingMethod()函數的返回值,也就是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()來的簡單啊?比如我用Task的TaskContinueWith方法也能實現:
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。
參考:
死鎖問題?https://www.cnblogs.com/OpenCoder/p/4434574.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
?
引用如果我直接在Main方法里調用異步,你要把Main改成異步?不可能的
可以用同步方式調用異步方法,比如:
AsyncMethod().Wait();
var result = AsyncMethod().Result;
?
111 balabala. My Thread ID is :1
Helo I am TimeConsumingMethod. My Thread ID is :3
Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :3
Hello I am TimeConsumingMethod + AsyncMethod. My Thread ID is :3
Hello I am TimeConsumingMethod + AsyncMethod. My Thread ID is :3
222 balabala. My Thread ID is :1
我的輸出結果
所以并不是這么容易就出現死鎖,而是變成了同步執行,等待執行結果了吧
?
總結
以上是生活随笔為你收集整理的C# 彻底搞懂async/await的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 传《海王2》将删除海后戏份 希尔德发言人
- 下一篇: 鹏华半导体芯片etf怎么买?一文弄懂