.NET 謹(jǐn)用 async/await

C# 5.0 引入 async/await 關(guān)鍵字,旨在簡(jiǎn)化異步編程模型,拋去語(yǔ)法糖就是 Net4.0 的 Task + 狀態(tài)機(jī)。其實(shí)在處理異步編程使用 Task 還是挺簡(jiǎn)單的,不過(guò)既然推出了新的語(yǔ)法糖,難免會(huì)嘗試一下,然而在使用中卻沒(méi)想象中那么單純。以下針對(duì) ASP.NET 應(yīng)用程序?qū)嶋H使用過(guò)程中的一些總結(jié), 包括 異常捕獲、死鎖、應(yīng)用程序崩潰,實(shí)際使用過(guò)程中一不注意就可能掉坑里了。

異常捕獲

async 方法有三種返回類(lèi)型: void、Task、Task<T>

async void

該方式聲明的方法是無(wú)法使用 catch 捕獲異常的,所以以下代碼的 try、catch 并沒(méi)什么卵用。

private static async void ThrowExceptionAsync()
{
  await Task.Delay(1000);
  throw new Exception("拋個(gè)異常玩玩");
}

public static async void CatchAsyncVoidException()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception ex)
  {
    throw ex;
  }
}
async Task 或 async Task<T>

這兩種方式聲明的方法異常信息會(huì)包含Task屬性?xún)?nèi),但前提需要在try里面使用 await 等待。

private static async Task ThrowExceptionAsync()
{
  await Task.Delay(1000);
  throw new Exception("拋個(gè)異常玩玩");
}

public static async Task CatchAsyncTaskException()
{
  try
  {
    await ThrowExceptionAsync();
  }
  catch (Exception ex)
  {
    throw ex;
  }
}
TaskScheduler.UnobservedTaskException

未捕獲的 Task 異常信息可以通過(guò)設(shè)置全局的 TaskScheduler.UnobservedTaskException 來(lái)記錄錯(cuò)誤日志,在 Global.asax 增加如下代碼:

void Application_Start(object sender, EventArgs e)
{
  // 在應(yīng)用程序啟動(dòng)時(shí)運(yùn)行的代碼
  TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskExceptionException;
}

void TaskScheduler_UnobservedTaskExceptionException(object sender, UnobservedTaskExceptionEventArgs e)
{
  if (e.Exception != null)
  {
    // do something
  }
}

同步上下文

異步編程必然是關(guān)于線程的使用,線程有一個(gè)同步上下文的概念,個(gè)人認(rèn)為線程同步上下文是 async/await 遇到最揪心的問(wèn)題。在現(xiàn)有項(xiàng)目開(kāi)發(fā)中我們可能想嘗試使用 async/await,但老代碼都是同步方式,這時(shí)如果調(diào)用一個(gè)聲明為 async 的方法,死鎖和應(yīng)用程序崩潰的問(wèn)題一不小心就可能出現(xiàn)。

注意: 控制臺(tái)程序和 .NET Core 程序 將不會(huì)遇到這個(gè)問(wèn)題,它們不需要同步上下文。

死鎖
private static async Task XXXAsync()
{
  await Task.Delay(1000);
  // some code
}

public static void Test()
{
  var task = XXXAsync();
  task.Wait();
}

以上代碼很完美的實(shí)現(xiàn)了死鎖。 默認(rèn)情況下,當(dāng) Wait() 未完成的 Task 時(shí),會(huì)捕獲當(dāng)前線程上下文,在 Task 完成時(shí)使用該上下文恢復(fù)方法的執(zhí)行。 當(dāng) async 方法內(nèi)的 await 執(zhí)行完成時(shí),它會(huì)嘗試獲取調(diào)用者線程所在的上下文執(zhí)行方法的剩余部分, 但是該上下文已含有一個(gè)線程,該線程在等待 async 方法完成。然后它們相互等待對(duì)方,然后就沒(méi)有然后了,死在那里。

針對(duì)死鎖問(wèn)題的解決方式是增加 ConfigureAwait(false)

// await Task.Delay(1000);
await Task.Delay(1000).ConfigureAwait(false);  // 解決死鎖

當(dāng) await 等待完成時(shí),它會(huì)嘗試在線程池上下文中執(zhí)行 async 方法的剩余部分,因此就不存在死鎖。

應(yīng)用程序崩潰

測(cè)試環(huán)境總發(fā)現(xiàn)IIS應(yīng)用程序池總是崩潰,到底是什么原因?當(dāng)時(shí)我們對(duì)這個(gè)問(wèn)題也是非常懵逼,代碼看上去并沒(méi)什么明顯毛病,試圖欺騙自己應(yīng)該是環(huán)境本身的問(wèn)題吧,但事實(shí)上確實(shí)是在代碼中下了毒。通過(guò)各種資料查閱和測(cè)試,基本可以斷定是同步代碼調(diào)用異步代碼,同步上下文引起的問(wèn)題。

如果調(diào)用一個(gè) async 方法。如果使用 await 等待,當(dāng)前線程立馬被釋放回線程池,線程的上下文信息會(huì)被保存。如果沒(méi)有使用 await(async void 的方法,必然沒(méi)有辦法使用 await),調(diào)用 async 方法之后,代碼會(huì)繼續(xù)往下執(zhí)行,執(zhí)行完成后當(dāng)前線程被釋放回線程池,線程的上下文信息不會(huì)被保存。當(dāng) async 中的異步任務(wù)執(zhí)行完成后,會(huì)從線程池中獲取一個(gè)線程繼續(xù)執(zhí)行剩余代碼,同時(shí)會(huì)獲取當(dāng)初調(diào)用者所在線程的上下文信息(如果當(dāng)初調(diào)用者所在線程沒(méi)有釋放回線程池,上下文信息可以獲取到)。那么問(wèn)題就來(lái)了,如果當(dāng)初調(diào)用者沒(méi)有使用 await 并且 所在線程釋放回線程池了,上下文信息因?yàn)闆](méi)有被保持下來(lái),就獲取不到了,這時(shí)候會(huì)拋出異常 未將對(duì)象引用設(shè)置到對(duì)象的實(shí)例,經(jīng)過(guò)測(cè)試這個(gè)異常信息并不一定每次都會(huì)出現(xiàn),原因和線程的釋放有關(guān),調(diào)用者所在線程的上下文信息存在就不會(huì)拋出異常。異常錯(cuò)誤信息如下,這個(gè)異常最終會(huì)導(dǎo)致應(yīng)用程序集停止。

   在 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)
--- 引發(fā)異常的上一位置中堆棧跟蹤的末尾 ---
   在 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()

針對(duì)以上異常我們有什么方式可以解決呢?依然是ConfigureAwait(false),在 Task 上加上 ConfigureAwait(false),此設(shè)置代表當(dāng) async 中的異步任務(wù)完成后,不讀取當(dāng)時(shí)調(diào)用它的原線程的上下文信息,而是在線程池上下文中執(zhí)行 async 方法的剩余部分。

public static Task XXXAsync()
{
  await Task.Run(() =>
  {
    // some code
  }).ConfigureAwait(false);
}

總結(jié)

  1. 盡量不混合使用同步和異步代碼,要異步就異步到底;
  2. 善用 ConfigureAwait(false);

參考鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容