如何处理 Task.Run 异常

How to handle Task.Run Exception

我在捕获来自 Task.Run 的异常时遇到了问题,通过如下更改代码解决了这个问题。我想知道这两种方式处理异常的区别:

Outside 方法中我无法捕获异常,但在 Inside 方法中我可以。

void Outside()
{
    try
    {
        Task.Run(() =>
        {
            int z = 0;
            int x = 1 / z;
        });
    }
    catch (Exception exception)
    {
        MessageBox.Show("Outside : " + exception.Message);
    }
}

void Inside()
{
    Task.Run(() =>
    {
        try
        {
            int z = 0;
            int x = 1 / z;
        }
        catch (Exception exception)
        {
            MessageBox.Show("Inside : "+exception.Message);
        }
    });
}

当任务 运行 时,它抛出的任何异常都将被保留并在等待任务结果或任务完成时重新抛出。

Task.Run() returns 一个 Task 对象,你可以用它来做到这一点,所以:

var task = Task.Run(...)

try
{
    task.Wait(); // Rethrows any exception(s).
    ...

对于较新版本的 C#,您可以使用 await 而不是 Task.Wait():

try
{
    await Task.Run(...);
    ...

更整洁。


为了完整起见,这里有一个可编译的控制台应用程序,演示了 await:

的用法
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            test().Wait();
        }

        static async Task test()
        {
            try
            {
                await Task.Run(() => throwsExceptionAfterOneSecond());
            }

            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        static void throwsExceptionAfterOneSecond()
        {
            Thread.Sleep(1000); // Sleep is for illustration only. 
            throw new InvalidOperationException("Ooops");
        }
    }
}

对我来说,我希望我的 Task.Run 在出错后继续,让 UI 在有时间的时候处理错误。

我的(奇怪?)解决方案是也有一个 Form.Timer 运行。我的 Task.Run 有它的队列(对于 long-运行 非 UI 的东西),我的 Form.Timer 有它的队列(对于 UI 的东西)。

因为这个方法已经为我工作,添加错误处理是微不足道的:如果 task.Run 得到一个错误,它将错误信息添加到 Form.Timer 队列,显示错误对话框。

使用 Task.Wait 的想法可以解决问题,但会导致调用线程(如代码所说)等待并因此阻塞,直到任务完成,这有效地使代码同步而不是异步.

而是使用Task.ContinueWith选项来实现结果:

Task.Run(() =>
{
   //do some work
}).ContinueWith((t) =>
{
   if (t.IsFaulted) throw t.Exception;
   if (t.IsCompleted) //optionally do some work);
});

如果任务需要在 UI 线程上继续,请使用 TaskScheduler.FromCurrentSynchronizationContext() 选项作为继续的参数,如下所示:

).ContinueWith((t) =>
{
    if (t.IsFaulted) throw t.Exception;
    if (t.IsCompleted) //optionally do some work);
}, TaskScheduler.FromCurrentSynchronizationContext());

此代码将简单地从任务级别重新抛出聚合异常。当然你也可以在这里介绍一些其他形式的异常处理。

在您的外部代码中,您只检查启动任务是否不抛出异常而不是任务主体本身。它异步运行,然后启动它的代码完成。

您可以使用:

void Outside()
{
    try
    {
        Task.Run(() =>
        {
            int z = 0;
            int x = 1 / z;
        }).GetAwaiter().GetResult();
    }
    catch (Exception exception)
    {
        MessageBox.Show("Outside : " + exception.Message);
    }
}

使用 .GetAwaiter().GetResult() 等到任务结束并按原样传递抛出的异常,而不将它们包装在 AggregateException.

您可以等待,然后异常会冒泡到当前同步上下文(请参阅 Matthew Watson 的回答)。或者,正如 Menno Jongerius 提到的,您可以 ContinueWith 使代码保持异步。请注意,只有在使用 OnlyOnFaulted 延续选项抛出异常时才能这样做:

Task.Run(()=> {
    //.... some work....
})
// We could wait now, so we any exceptions are thrown, but that 
// would make the code synchronous. Instead, we continue only if 
// the task fails.
.ContinueWith(t => {
    // This is always true since we ContinueWith OnlyOnFaulted,
    // But we add the condition anyway so resharper doesn't bark.
    if (t.Exception != null)  throw t.Exception;
}, default
     , TaskContinuationOptions.OnlyOnFaulted
     , TaskScheduler.FromCurrentSynchronizationContext());

启用选项 "Just My Code" 时,Visual Studio 在某些情况下会在抛出异常的行中断并显示一条错误消息:

Exception not handled by user code.

这个错误是良性的。您可以按 F5 继续并查看这些示例中演示的异常处理行为。为防止 Visual Studio 在第一个错误时中断,只需禁用 工具 > 选项 > 调试 > 常规下的仅我的代码复选框。

在@MennoJongerius 的基础上回答以下内容保持异步并将异常从 .wait 移至 Async Completed 事件处理程序:

Public Event AsyncCompleted As AsyncCompletedEventHandler

).ContinueWith(Sub(t)
                   If t.IsFaulted Then
                       Dim evt As AsyncCompletedEventHandler = Me.AsyncCompletedEvent
                       evt?.Invoke(Me, New AsyncCompletedEventArgs(t.Exception, False, Nothing))
                   End If
               End Sub)