task async await 異常處理

今天跟前端對接支付接口碰到一個問題比較有代表性的問題,接口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使用需要多注意,很複雜。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章