今天跟前端對接支付接口碰到一個問題比較有代表性的問題,接口throw異常,調用鏈路的所有服務都沒有catch,但是返回到前端的結果還是成功的,這裏跟蹤的麻煩點就是調用鏈路比較深,中間調用了5個左右的服務,跟蹤來跟蹤去還是沒發現問題,最後要同事確認下,最後一個接口有沒有給我拋出異常,同事很肯定的說返回了異常出來,最後發現倒數第二個接口確實存在問題,
public async Task<object> test()
{
test1();
return "xxx";
}
private async Task test1()
{
var response = A.底層支付接口();
if (response.code != 200)
{
throw new Exception();
}
await Task.Run(...);
}
以上代碼不是原始代碼,但是跟這份代碼結構類似,我們看下這兩個方法,都是async方法,看似跟普通同步方法沒啥區別,其實底層執行邏輯區別很大,接着以前il代碼的分析,被async修飾的方法會被編譯器編譯成class,並且實現狀態機接口,最後我們的test方法裏面的內容在狀態機movenext方法裏面被調用,看下il代碼
.try
{
......
Monix.Wallet.Service.Controllers.PaymentDeskController Monix.Wallet.Service.Controllers.PaymentDeskController/'<test2>d__4'::'<>4__this'
IL_001e: call instance class [mscorlib]System.Threading.Tasks.Task Monix.Wallet.Service.Controllers.PaymentDeskController::test22()
IL_0034: ldnull
IL_0035: stloc.1 // V_1
IL_0036: leave.s IL_0050
} // end of .try
catch [mscorlib]System.Exception
{
...
valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::SetException(class [mscorlib]System.Exception)
IL_004d: nop
IL_004e: leave.s IL_0065
} // end of catch
....
} // end of method '<test2>d__4'::MoveNext
大概長上面那樣,發現問題沒?test1方法裏面的throw被編譯器暫時消化掉了,編譯器編譯時會默認加上try catch,那這個怎麼處理,我task裏面的異常怎麼拿到?兩種方案,要麼await表達式,要麼調用同步方法wait(),正確操作應該是
public async Task<object> test()
{
await test1();
return "xxx";
}
private async Task test1()
{
var response = A.底層支付接口();
if (response.code != 200)
{
throw new Exception();
}
await Task.Run(...);
}
那麼這有啥本質區別呢?首先編譯器會把await表達式前後定義兩種狀態邏輯,後面的邏輯會被阻塞住(這裏的阻塞可能是線程),並且hook表達式await的結果result,當hook到任務task已完成,jit會調用getResult()方法獲取結果,這裏有個細節需要注意了,如果task在執行過程中有異常,這時候它纔會跑出來,我們可以簡單看下GetResult()方法的實現。
private static void ThrowForNonSuccess(Task task)
{
....
switch (task.Status)
{
....
case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
if (edis.Count > 0)
{
edis[0].Throw();
Debug.Fail("Throw() should have thrown");
break;
}
else
{
Debug.Fail("There should be exceptions if we're Faulted.");
throw task.Exception!;
}
}
}
以上代碼就是GetResult()方法,間接調用的邏輯,task在執行過程中產生的異常會被Add到ExceptionDispatchInfo這麼一個對象裏面,並且它內部維護了集合,具體邏輯大概就是這樣,那這個ExceptionDispatchInfo對象什麼時候設置異常的呢?看上面的il代碼在catch裏面調用了task的setException方法,該方法裏面做了add操作。就到這吧,task async await使用需要多注意,很複雜。