async / await 使異步代碼更容易寫,由於它隱藏了不少細節。 許多這些細節都捕獲在 SynchronizationContext 中,這些可能會改變異步代碼的行爲徹底因爲你執行你的代碼的環境(例如WPF,Winforms,控制檯或ASP.NET)所控制。 若果嘗試經過忽略 SynchronizationContext 產生的影響,您可能遇到死鎖和競爭條件情況。html
SynchronizationContext 控制任務連續的調度方式和位置,而且有許多不一樣的上下文可用。 若是你正在編寫一個 WPF 應用程序,構建一個網站或使用 ASP.NET 的API,你應該知道你已經使用了一個特殊的 SynchronizationContext 。瀏覽器
讓咱們來看看控制檯應用程序中的一些代碼:安全
public class ConsoleApplication { public static void Main() { Console.WriteLine($"{DateTime.Now.ToString("T")} - Starting"); var t1 = ExecuteAsync(() => Library.BlockingOperation()); var t2 = ExecuteAsync(() => Library.BlockingOperation())); var t3 = ExecuteAsync(() => Library.BlockingOperation())); Task.WaitAll(t1, t2, t3); Console.WriteLine($"{DateTime.Now.ToString("T")} - Finished"); Console.ReadKey(); } private static async Task ExecuteAsync(Action action) { // Execute the continuation asynchronously await Task.Yield(); // The current thread returns immediately to the caller // of this method and the rest of the code in this method // will be executed asynchronously action(); Console.WriteLine($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}"); } }
其中 Library.BlockingOperation() 是一個第三方庫,咱們用它來阻塞正在使用的線程。 它能夠是任何阻塞操做,可是爲了測試的目的,您可使用 Thread.Sleep(2) 來代替實現。app
16:39:15 - Starting
16:39:17 - Completed task
on
thread 11
16:39:17 - Completed task
on
thread 10
16:39:17 - Completed task
on
thread 9
16:39:17 - Finished
在示例中,咱們建立三個任務阻塞線程一段時間。 Task.Yield 強制一個方法是異步的,經過調度這個語句以後的全部內容(稱爲_continuation_)來執行,但當即將控制權返回給調用者(Task.Yield 是告知調度者"我已處理完成,能夠將執行權讓給其餘的線程",至於最終調用哪一個線程,由調度者決定,可能下一個調度的線程仍是本身自己)。 從輸出中能夠看出,因爲 Task.Yield 全部的操做最終並行執行,總執行時間只有兩秒。框架
假設咱們想在 ASP.NET 應用程序中重用這個代碼,咱們將代碼 Console.WriteLine 轉換爲 HttpConext.Response.Write 便可,咱們能夠看到頁面上的輸出:異步
public class HomeController : Controller { public ActionResult Index() { HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting"); var t1 = ExecuteAsync(() => Library.BlockingOperation())); var t2 = ExecuteAsync(() => Library.BlockingOperation())); var t3 = ExecuteAsync(() => Library.BlockingOperation())); Task.WaitAll(t1, t2, t3); HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished"); return View(); } private async Task ExecuteAsync(Action action) { await Task.Yield(); action(); HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}"); } }
咱們會發現,在瀏覽器中啓動此頁面後不會加載。 看來咱們是引入了一個死鎖。那麼這裏到底發生了什麼呢?async
死鎖的緣由是控制檯應用程序調度異步操做與 ASP.NET 不一樣。 雖然控制檯應用程序只是調度線程池上的任務,而 ASP.NET 確保同一 HTTP 請求的全部異步任務都按順序執行。 因爲 Task.Yield 將剩餘的工做排隊,並當即將控制權返回給調用者,所以咱們在運行 Task.WaitAll 的時候有三個等待操做。 Task.WaitAll 是一個阻塞操做,相似的阻塞操做還有如 Task.Wait 或 Task.Result,所以阻止當前線程。測試
ASP.NET 是在線程池上調度它的任務,阻塞線程並非致使死鎖的緣由。 可是因爲是順序執行,這致使不容許等待操做開始執行。 若是他們沒法啓動,他們將永遠不能完成,被阻止的線程不能繼續。網站
此調度機制由 SynchronizationContext 類控制。 每當咱們等待任務時,在等待的操做完成後,在 await 語句(即繼續)以後運行的全部內容將在當前 SynchronizationContext 上被調度。 上下文決定了如何、什麼時候和在何處執行任務。 您可使用靜態 SynchronizationContext.Current 屬性訪問當前上下文,而且該屬性的值在 await 語句以前和以後始終相同。this
在控制檯應用程序中,SynchronizationContext.Current 始終爲空,這意味着鏈接能夠由線程池中的任何空閒線程拾取,這是在第一個示例中能並行執行操做的緣由。 可是在咱們的 ASP.NET 控制器中有一個 AspNetSynchronizationContext,它確保前面提到的順序處理。
要點一:
不要使用阻塞任務同步方法,如 Task.Result,Task.Wait,Task.WaitAll 或 Task.WaitAny。 控制檯應用程序的 Main 方法目前是該規則惟一的例外(由於當它們得到徹底異步時的行爲會有所改變)。
如今咱們知道不該該使用 Task.WaitAll,讓咱們修復咱們的控制器的 Index Action:
public async Task<ActionResult> Index() { HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting "); var t1 = ExecuteAsync(() => Library.BlockingOperation())); var t2 = ExecuteAsync(() => Library.BlockingOperation())); var t3 = ExecuteAsync(() => Library.BlockingOperation())); await Task.WhenAll(t1, t2, t3); HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished "); return View(); }
咱們將 Task.WaitAll(t1,t2,t3)更改成非阻塞等待 Task.WhenAll(t1,t2,t3),這也要求咱們將方法的返回類型從 ActionResult 更改成 async 任務。
更改後咱們看到頁面上輸出以下結果:
16:41:03 - Starting
16:41:05 - Completed task
on
thread 60
16:41:07 - Completed task
on
thread 50
16:41:09 - Completed task
on
thread 74
16:41:09 - Finished
要點二:
永遠不要假設異步代碼是以並行方式執行的,除非你顯式地將其設置爲並行執行。 用 Task.Run 或 Task.Factory.StartNew 調度異步代碼來使他們並行運行。
咱們使用新的的規則:
private async Task ExecuteAsync(Action action) { await Task.Yield(); action(); HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} "); }
to:
private async Task ExecuteAsync(Action action) { await Task.Run(action); HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} "); }
Task.Run 在沒有 SynchronizationContext 的狀況下在線程池上調度給定的操做。 這意味着在任務內運行的全部內容都將 SynchronizationContext.Current 設置爲 null。 結果是全部入隊操做均可以由任何線程自由選取,而且它們沒必要遵循ASP.NET上下文指定的順序執行順序。 這也意味着任務可以並行執行。
注意 HttpContext 不是線程安全的,所以咱們不該該在 Task.Run 中訪問它,由於這可能在 html 輸出上產生奇怪的結果。 可是因爲上下文捕獲,Response.Write 被確保發生在 AspNetSynchronizationContext(這是在 await 以前的當前上下文)中,確保對 HttpContext 的序列化訪問。
此次的輸出結果爲:
16:42:27 - Starting
16:42:29 - Completed task
on
thread 9
16:42:29 - Completed task
on
thread 12
16:42:29 - Completed task
on
thread 14
16:42:29 - Finished
class AsyncHelper { public static async Task ExecuteAsync(Action action) { await Task.Run(action); HttpContext.Current.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} "); } }
咱們剛剛將 HttpContext.Response 更改成靜態可用的 HttpContext.Current.Response 。 這仍然能夠工做,這得益於 AspNetSynchronizationContext,但若是你嘗試在 Task.Run 中訪問 HttpContext.Current ,你會獲得一個 NullReferenceException,由於 HttpContext.Current 沒有設置。
正如咱們在前面的例子中看到的,上下文捕獲能夠很是方便。 可是在許多狀況下,咱們不須要爲 "continuation" 恢復的上下文。 上下文捕獲是有代價的,若是咱們不須要它,最好避免這個附加的邏輯。 假設咱們要切換到日誌框架,而不是直接寫入加載的網頁。 咱們重寫咱們的幫助:
class AsyncHelper { public static async Task ExecuteAsync(Action action) { await Task.Run(action); Log.Info($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}"); } }
如今在 await 語句以後,AspNetSynchronizationContext 中沒有咱們須要的東西,所以在這裏不恢復它是安全的。 在等待任務以後,可使用 ConfigureAwait(false) 禁用上下文捕獲。 這將告訴等待的任務調度其當前 SynchronizationContext 的延續。 由於咱們使用 Task.Run,上下文是 null,所以鏈接被調度在線程池上(沒有順序執行約束)。
使用 ConfigureAwait(false) 時要記住的兩個細節:
因爲異步代碼的 SynchronizationContext,異步代碼在不一樣環境中的表現可能不一樣。 可是,當遵循最佳作法時,咱們能夠將遇到問題的概率減小到最低限度。 所以,請確保您熟悉 async/await 最佳實踐並堅持使用它們。
原文: Context Matters