1. 程式人生 > 其它 >如何使用 C# 在非同步程式碼中處理異常

如何使用 C# 在非同步程式碼中處理異常

異常處理是一種處理執行時錯誤的技術,而 非同步程式設計 允許我們在處理資源密集型的業務邏輯時不需要在 Main 方法或者在 執行執行緒 中被阻塞,值得注意的是,非同步方法和同步方法的異常處理機制是不一樣的,本篇我們就來討論下如何在非同步方法中處理異常。

非同步方法 VS 同步方法 的異常處理

在同步程式碼中丟擲異常,它會一直以冒泡的方式往上拋,直到遇到可以處理這個異常的 catch 塊為止,可以想象,非同步方法中的異常丟擲肯定要比這個複雜。

大家都知道 非同步方法 可以有三種返回型別,如:void, Task, Task<TResult>,當異常方法的返回值是 Task ,Task<TResult>

的方法中丟擲異常的話,這個異常物件會被塞到 AggregateException 物件中,然後包裹在 Task 中進行返回,有些朋友可能要問,如果非同步方法中丟擲了幾個異常怎麼辦?其實也是一樣的道理,這些異常物件都會被塞到 AggregateException 中通過 Task 去返回。

最後,如果異常出現在返回值為 void 的非同步方法中,異常是在呼叫這個非同步方法的 SynchronizationContext 同步上下文上觸發。

返回 void 非同步方法中的異常

下面的程式展示了返回 void 的非同步方法中丟擲了異常。


    class Program
    {
        static void Main(string[] args)
        {
            ThisIsATestMethod();

            Console.ReadLine();
        }

        public static void ThisIsATestMethod()
        {
            try
            {
                AsyncMethodReturningVoid();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        private static async void AsyncMethodReturningVoid()
        {
            await Task.Delay(1000);
            throw new Exception("This is an error message...");
        }
    }

從圖中可以看到,AsyncMethodReturningVoid 方法丟擲的異常會被包裹此方法的 try catch 捕捉到。

返回 Task 的非同步方法異常

當異常從返回值為 Task 的非同步方法中丟擲,這個異常物件會被包裹在 Task 中並且返回給方法呼叫方,當你用 await 等待此方法時,只會得到一組異常中的第一個被觸發的異常,如果有點懵的話,如下程式碼所示:


    class Program
    {
        static void Main(string[] args)
        {
            ExceptionInAsyncCodeDemo();

            Console.ReadLine();
        }

        public static async Task ExceptionInAsyncCodeDemo()
        {
            try
            {
                var task1 = Task.Run(() => throw new IndexOutOfRangeException("IndexOutOfRangeException is thrown."));
                var task2 = Task.Run(() => throw new ArithmeticException("ArithmeticException is thrown."));
                await Task.WhenAll(task1, task2);
            }
            catch (AggregateException ex)
            {
                Console.WriteLine(ex.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

從上面程式碼中可以看出 task1 和 task2 都會丟擲異常,但在 catch 塊中只捕獲了 task1 中的異常,這就說明返回值為 Task 的多個異常的方法中,呼叫方只能截獲第一次發生異常的異常物件。

使用 Exceptions 屬性 獲取所有異常

要想獲取已丟擲的所有異常,可以利用 Task.Exceptions 屬性來獲取,下面的程式碼清單展示瞭如何在返回 Task 的方法中獲取所有的異常資訊。


    class Program
    {
        static void Main(string[] args)
        {
            ExceptionInAsyncCodeDemo();

            Console.ReadLine();
        }

        public static async Task ExceptionInAsyncCodeDemo()
        {
            Task tasks = null;
            try
            {
                var task1 = Task.Run(() => throw new IndexOutOfRangeException("IndexOutOfRangeException is thrown."));
                var task2 = Task.Run(() => throw new ArithmeticException("ArithmeticException is thrown."));
                tasks = Task.WhenAll(task1, task2);
                await tasks;
            }
            catch
            {
                AggregateException aggregateException = tasks.Exception;

                foreach (var e in aggregateException.InnerExceptions)
                {
                    Console.WriteLine(e.GetType().ToString());
                }
            }
        }
    }

使用 AggregateException.Handle 處理所有異常

你可以利用 AggregateException.Handle 屬性去處理一組異常中的某一個,同時忽略其他你不關心的異常,下面的程式碼片段展示瞭如何去實現。


    class Program
    {
        static async Task Main(string[] args)
        {
            await ExceptionInAsyncCodeDemo();
            Console.Read();
        }

        public static async Task ExceptionInAsyncCodeDemo()
        {
            Task tasks = null;
            try
            {
                var task1 = Task.Run(() => throw new IndexOutOfRangeException("IndexOutOfRangeException is thrown."));
                var task2 = Task.Run(() => throw new ArithmeticException("ArithmeticException is thrown."));
                tasks = Task.WhenAll(task1, task2);
                await tasks;
            }
            catch(AggregateException ex)
            {
                AggregateException aggregateException = tasks.Exception;

                foreach (var e in aggregateException.InnerExceptions)
                {
                    Console.WriteLine(e.GetType().ToString());
                }
            }
        }
    }

上面的程式碼片段表示:IndexOutOfRangeException 會被處理, InvalidOperationException 會被忽略。

最後想說的是,你可以利用 非同步程式設計 來提高程式的擴充套件性和吞吐率,當你在使用非同步方法時,請注意在非同步方法中的異常處理語義和同步方法中的異常處理是不一樣的。

譯文連結:https://www.infoworld.com/article/3453659/how-to-handle-exceptions-in-asynchronous-code-in-c.html