1. 程式人生 > 其它 >task async await 異常處理

task async await 異常處理

今天跟前端對接支付介面碰到一個奇葩問題,支付介面報錯了,但是返回到前端的結果還是成功的,這裡簡單說一下整個呼叫鏈路,前端調我的api介面,我再調其他服務,其他服務再調同事的介面,最後呼叫同事的底層支付介面(這部分程式碼我沒有),跟蹤來跟蹤去還是沒發現問題,最後要同事確認下,底層的支付介面有沒有給我丟擲異常,同事很肯定的說返回了異常出來,最後仔細跟蹤倒數第二個介面發現了問題,  
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使用需要多注意,很複雜。