傳統asp.net小心 async/await坑
最近在改老專案時,幹了一件自以為很有成就感的事,心想 “專案都是同步方法,為啥不用非同步方法呢?”,於是有了非同步方法,型別下面的程式碼(當然是舉例子說明啊)
//更新某人名下公司名稱 public Task<bool> UpdateUser(string id,string companyName) { var usrInfo=Db.GetUsrInfo(id); var flag= await Db.UpdateCompanyNameAsync(usrInfo.companyId,companyName); return flag }
“咋一看,好像沒啥問題,不就是根據id更新名稱嗎?”
可實際在測試的時候,報錯了,型別下面的錯誤
在 System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext) 在 System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext) 在 System.Web.LegacyAspNetSynchronizationContext.CallCallbackPossiblyUnderLock(SendOrPostCallback callback, Object state) 在 System.Web.LegacyAspNetSynchronizationContext.CallCallback(SendOrPostCallback callback, Object state) 在 System.Web.LegacyAspNetSynchronizationContext.Post(SendOrPostCallback callback, Object state) 在 System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.PostAction(Object state) 在 System.Threading.Tasks.AwaitTaskContinuation.RunCallback(ContextCallback callback, Object state, Task& currentTask) --- 引發異常的上一位置中堆疊跟蹤的末尾 --- 在 System.Threading.Tasks.AwaitTaskContinuation.<>c.<ThrowAsyncIfNecessary>b__18_0(Object s) 在 System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state) 在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) 在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx) 在 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() 在 System.Threading.ThreadPoolWorkQueue.Dispatch() 在 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
注意:這個錯誤,在非同步方法裡用了同步的方法導致的。
有同學此時可能會有疑問,"這個為啥會報這種錯誤呢"?
別急,這個就涉及到了 “同步上下文”
同步上下文
非同步程式設計必然是關於執行緒的使用,執行緒有一個同步上下文的概念,個人認為執行緒同步上下文是 async/await 遇到最揪心的問題。在現有專案開發中我們可能想嘗試使用 async/await,但老程式碼都是同步方式,這時如果呼叫一個宣告為 async 的方法,死鎖和應用程式崩潰的問題一不小心就可能出現。
注意: 控制檯程式和.Net Core程式 將不會遇到這個問題,它們不需要同步上下文。
死鎖
private static async Task TestAsync() { await Task.Delay(1000); // 業務程式碼 } public static void TestOne() { var task = TestAsync(); task.Wait(); }
以上程式碼很完美的實現了死鎖。 預設情況下,當 Wait() 未完成的 Task 時,會捕獲當前執行緒上下文,在 Task 完成時使用該上下文恢復方法的執行。 當 async 方法內的 await 執行完成時,它會嘗試獲取呼叫者執行緒所在的上下文執行方法的剩餘部分, 但是該上下文已含有一個執行緒,該執行緒在等待 async 方法完成。然後它們相互等待對方,然後就沒有然後了,死在那裡。
針對死鎖問題的解決方式是增加 ConfigureAwait(false)
// await Task.Delay(1000); await Task.Delay(1000).ConfigureAwait(false); // 解決死鎖
當 await 等待完成時,它會嘗試線上程池上下文中執行 async 方法的剩餘部分,因此就不存在死鎖。
如果專案中,有同步程式碼,有有很多的非同步程式碼,執行非同步程式碼時的引數是通過同步程式碼所獲取的,那麼專案中很有可能會有上述的異常資訊
經過查閱資料,和檢視園子裡其他大佬們的文章瞭解到
當呼叫一個 async 方法。如果使用 await 關鍵字,當前執行緒立馬被釋放回執行緒池,執行緒的上下文資訊會被儲存。如果沒有使用 await(async void 的方法,必然沒有辦法使用 await),呼叫 async 方法之後,程式碼會繼續往下執行,執行完成後當前執行緒被釋放回執行緒池,執行緒的上下文資訊不會被儲存。當 async 中的非同步任務執行完成後,會從執行緒池中獲取一個執行緒繼續執行剩餘程式碼,同時會獲取當初呼叫者所線上程的上下文資訊(如果當初呼叫者所線上程沒有釋放回執行緒池,上下文資訊可以獲取到)。那麼問題就來了,如果當初呼叫者沒有使用 await 並且 所線上程釋放回執行緒池了,上下文資訊因為沒有被保持下來,就獲取不到了,這時候會丟擲異常 未將物件引用設定到物件的例項,經過測試這個異常資訊並不一定每次都會出現,原因和執行緒的釋放有關,呼叫者所線上程的上下文資訊存在就不會丟擲異常。