C#并发编程 --- 异步方法的异常处理

引言

现在模拟一个异步方法抛出了异常:

public static async Task ThrowAfter(int ms, string message)
{
    await Task.Delay(ms);
    thrownew Exception(message);
}

思考一下, DontHandle() 方法是否能够捕获到异常?.

public static void DontHandle()
{
    try
    {
        ThrowAfter(1000, "first");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

}

答案是:不会捕获到异常!

因为 DontHandle() 方法在 ThrowAfter() 方法抛出异常之前,就已经执行完毕。

异步方法的异常处理

那么上述代码怎么才能捕获到异常呢?

若想要捕获异常则必须通过 await 关键字等待 ThrowAfter() 方法执行完成。

将上文中的代码段进行修改:

public static async void HandleoOnError()
{
    try
    {
        await ThrowAfter(1000, "first");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

结果就会输出:

first

多个异步方法的异常处理

如果调用两个异步方法,每个都会抛出异常,该如何处理呢?

我们可以这样写:

public static async void StartTwoTasks()
{
    try
    {
        await ThrowAfter(1000, "first");
        await ThrowAfter(1000, "second");
        Console.WriteLine("StartTwoTasks is Complate");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

思考一下输出是什么?

答案是:

first

并没有预想中的两个异常都捕获打印出来,也没有看到“StartTwoTasks is Complate”这句话打印出来。因为使用 await 关键字之后,两次调用 ThrowAfter() 方法就变成了同步执行,捕获到第一次的异常之后直接进入到 catch 代码段,不再执行后续代码。

可以尝试解决这个问题,使用 Task.WhenAll() 方法,该方法不管任务是否抛出异常,都会等到两个任务完成。如下代码:

public static async void StartTwoTasksParallel()
{
    try
    {
        Task t1 = ThrowAfter(1000, "first");
        Console.WriteLine("t1 is Complate");
        Task t2 = ThrowAfter(1000, "second");
        Console.WriteLine("t2 is Complate");
        await Task.WhenAll(t2, t1);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

输出:

t1 is Complate
t2 is Complate
second

从输出可以看出来,使用 WhenAll() 方法,两个任务都是执行完成的,但是,捕获异常只能捕获 WhenAll()方法参数中,排在最前面的,且第一个抛出异常的任务的消息,

上述方式有缺陷,只能抛出一个异常的任务的消息,可以将上面的方式再进化一下,如下代码:

public static async void StartTwoTasksParallelEx()
{
    Task t1 = null;
    Task t2 = null;
    try
    {
        t1 = ThrowAfter(1000, "first");
        t2 = ThrowAfter(1000, "second");
        await Task.WhenAll(t2, t1);

    }
    catch (Exception ex)
    {
        if (t1.IsFaulted)
        {
            Console.WriteLine(t1.Exception.InnerException.Message);
        }

        if (t2.IsFaulted)
        {
            Console.WriteLine(t2.Exception.InnerException.Message);
        }
    }
}

输出:

first
second

在 try/catch 代码块外声明任务变量t1、t2,使他们可以在 try/catch 块内访问,在这里,使用了IsFaulted 属性,检查任务的状态,若IsFaulted 属性为 true ,则表示该任务出现异常,就可以使用 Task.Exception.InnerException 访问异常本身。

使用AggregateException信息

除了上述方式外,还有一种更好的获取所有任务的异常信息的方式,Task.WhenAll() 方法返回的结果其实也是一个 Task 对象,而 Task 有一个 Exception 属性,它的类型是 AggregateException,是 Exception的一个派生类,AggregateException 类有一个 InnerExceptions 属性(异常集合,包含 Task.WhenAll() 方法列表中所有异常任务的异常信息)。

有了这个属性则可以轻松遍历所有异常。如下代码:

public static async void StartTwoTasksParallelEx2()
{
    Task t3 = null;
    try
    {
        Task t1 = ThrowAfter(1000, "first");
        Task t2 = ThrowAfter(1000, "second");
        await (t3 = Task.WhenAll(t2, t1));

    }
    catch (Exception ex)
    {
        foreach (var item in t3.Exception.InnerExceptions)
        {
            Console.WriteLine("InnerException:" + item.Message);
        }
    }
}

输出:

InnerException:second
InnerException:first

总结

除了前面提到的异步方法异常处理的基本知识点,以下是一些进阶的异常处理技巧:

  • 在异步方法中,如果需要将异常传递给调用方,请不要直接抛出异常。相反,应该使用 throw 关键字将异常包装在一个 Task 或 ValueTask 对象中,并将其返回给调用方。这可以避免在异步操作中丢失异常信息。

  • 如果需要在异步方法中处理多个异常,可以使用 catch 块来捕获不同类型的异常,并根据需要执行不同的处理操作。还可以使用 finally 块来执行清理操作,例如释放资源或恢复状态。

  • 如果需要在异步方法中执行一些异步操作,并且这些操作都必须成功才能继续执行下一步操作,那么可以使用 Task.WhenAll 方法来等待所有异步操作完成。如果任何一个异步操作失败,WhenAll 方法将返回一个 AggregateException 对象,其中包含所有失败的异常。

  • 如果需要在异步方法中执行多个异步操作,并且这些操作中的任何一个失败都将导致整个操作失败,那么可以使用 Task.WhenAny 方法来等待第一个异步操作完成。如果第一个操作失败,WhenAny 方法将返回一个 AggregateException 对象,其中包含第一个失败的异常。

  • 如果需要在异步方法中进行错误处理并且希望能够获取更多有关异常的信息,可以使用 ExceptionDispatchInfo 类。这个类可以捕获异常并将其存储在一个对象中,然后在需要时重新抛出异常。这可以帮助在异步操作中保留异常信息,并将其传递给调用方。

总之,在异步方法中处理异常时,需要注意一些细节和技巧,例如正确处理异常、捕获多个异常、等待多个异步操作、以及使用 ExceptionDispatchInfo 类来捕获异常。掌握这些处理技巧可以帮助编写更可靠、更健壮的异步代码。

欢迎关注我的个人博客: https://niuery.com/