小明用async/await寫了幾年的非同步方法,但總沒有完全理解裡面的機制,他決定去請教鄰居小花。
小花聽了小明的描述後說:首先你要明白非同步的根本是什麼?大白話解釋非同步就是:拉一個人(執行緒)幫著做一些耗時的事(下載、讀寫資料庫等),我先做別的事了(退出執行緒),等做好了和我說下,我再繼續做後面的事(恢復上下文)。
小花看到小時還沒有明白,就說:我舉個簡單例子幫你理解吧,假如有兩個方法A和B,A呼叫B方法,B方法是一個非同步方法,這時A不等待B執行完,如圖:
現在兩個方法被分隔幾個小塊,await關鍵字其實就用來隔開同步和非同步,上面的方法執行流程如下:
A呼叫B方法後,B方法在未執行到await之前還是同步方法,比如輸出Sub1還是在當前執行緒中執行,當方法遇到await後,就會把await後的方法放到新的執行緒中執行,當前執行緒則退出函式,由於呼叫的地方並沒有await,則主執行緒會繼續執行並輸出Part2,然後結束。等新執行緒中Thread.Sleep(5000)執行完後,會執行到Console.Write("Sub2");這一行程式碼會回到原來的執行緒執行,其實遇到await時會捕獲當前執行緒的執行上下文,然後給到新執行緒,新執行緒在執行完耗時操作後,會判斷之前捕獲到的執行上下方是否為null,如果不為null,則會在上下文中恢復並執行後面的方法,其實就是通過Tak的ContineWith方法註冊回撥,如圖:
小明好像聽懂了一些說:現在A方法呼叫DoSomethingAsync()並沒有等待,如果A方法需要這個方法執行完才能繼續執行,是不是要在DoSomethingAsync()前面加上await?小花回答是,並說:方法只要遇到await,就會把後面的方法給新執行緒執行,然後執行緒退出去執行別的方法,等新執行緒執行完後再通知當前執行緒恢復上下文繼續執行,如圖:
小明又問:你說非同步方法執行完後,後面的方法會在原來的執行緒中恢復並執行,如果我還想在新執行緒中繼續執行剩下的程式碼,要怎麼辦呢?小花說問的好,await呼叫新執行緒執行耗時操作時預設會捕獲當前上下文,如果不想捕獲,則可以呼叫ConfigAwait(false)方法,如圖:
執行流程如下:
小花補充到,上線提到的執行緒1、執行緒2、執行緒3等不一定準確,因為非同步的回撥是使用執行緒池中的執行緒,所以回撥有可能還在原來執行緒中執行,這個主要看作業系統的排程。
小明滿意的點點頭又問:我經常聽同事說用非同步方法會死鎖,這又是為什麼呢?小花聽了說,他們肯定是在呼叫非同步方法的時候使用.Result(),如圖:
小花指著圖解釋說:上面的程式碼task.Result()會阻塞執行緒等待task返回結果,DoSomethingAsync方法在執行完Thread.Sleep(5000)後,發現捕獲到的上下文不為空,則會嘗試將Console.Write("Sub2")這行程式碼交由呼叫執行緒去執行,而這時呼叫執行緒還在等待,就這樣互相卡著對方,就造成了死鎖,如圖:
小明點了點頭又問:那要怎麼避免呢?小花說出現這種情況也和框架有關,像WinForm為了讓所有UI操作都在主執行緒中執行,就新增了一個SynchronizationContext類例項用以表示當前上下文,而像控制檯等專案這個SynchronizationContext例項預設為null,所以即使使用.Result也不會死鎖。但最好使用非同步的時候不要用.Result,可以使用ConfigAwait(false)指明不捕獲上下文,或所有的方法全部非同步到底。
小明聽完滿意地回到自己的隔間。
更多精彩,請關注我的公眾號: