如何处理Task.Run异常

89

我曾遇到一个通过更改代码解决的从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);
        }
    });
}

1
请参考异常处理(任务并行库) - Sriram Sakthivel
3
如果 OP 没有使用 await,那么这不是一个重复的问题... 如果他正在使用 .net 4.x,他就不能使用 await - Matthew Watson
@MatthewWatson 你确定吗?await是.NET 4.x! - Matías Fidemraizer
@MohammadChamanpara 无论您是否使用async/await,答案都是相同的。 - Matías Fidemraizer
1
@MatíasFidemraizer 需要使用VS2012或更高版本。在VS2010中无法实现。我应该更准确地说,await需要C#5。 - Matthew Watson
显示剩余4条评论
7个回答

90

使用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());

这段代码将简单地从任务级别重新抛出聚合异常。当然,您也可以在此处引入其他形式的异常处理。


13
通常我会使用.ContinueWith(t => ...记录每个((AggregateException)t.Exception).Flatten().InnerExceptions...,TaskContinuationOptions.OnlyOnFaulted)来处理火灾和遗忘任务。 - Jürgen Steinblock
1
我不明白为什么要在另一个线程上运行任务,最终还是要等待它完成,你的答案保持了并行行为 :) - Thomas LAURENT
1
我不理解 "throw t.Exception"。这是什么意思?我尝试用 try/catch 包围 Task.Run,但没有捕获到任何异常。 - Craig
1
@thomas - 我认为这是用于处理的数据不会显示在响应中或在当前流程的其他部分中不需要的情况下。 - aj go

65

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

Task.Run() 返回一个 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");
        }
    }
}

6
同时,如果有异常的话,task.Result也会抛出异常。 - VMAtm
8
即使使用较新版本的C#,也不总是能够用await代替Task.Wait()。根据await (C# 参考):"使用 await 的异步方法必须要用 async 修饰符进行修改"。 - DavidRR
变量任务 = Task.Run(...),在 task.Wait() 调用同步后,当然要捕获异常,因为所有同步操作都是如此。 第二个调用的新版本 await Task.Run(...) 是异步的,同样你不能捕获异常。或者我搞砸了... :) - Nuri YILMAZ
@NuriYILMAZ 我猜你搞错了。;) 我附加了一个可编译的示例,以说明使用await捕获异常。请注意,Main()中的test.Wait()是为了允许调用异步方法 - 但它没有捕获异常 - await代码正在捕获异常。 - Matthew Watson
虽然与主题不是特别相关,但在异步代码中使用Thread.Sleep是一种不好的做法,而throwsExceptionAfterOneSecond就是这样的情况,因为它是一个阻塞调用。你应该使用Task.Delay()代替。 - Erman Akbay
3
不,我的例子中 throwsExceptionAfterOneSecond() 并不打算是异步的。它应该是一个同步方法,需要一段时间才能运行,然后抛出异常。如果它是异步的话,我们就不需要使用 Task.Run() 来运行它(你只需等待它),整个例子的意义也就失去了。请记住,这个例子旨在展示如何从 Task.Run() 获取异常。如果你将 throwsExceptionAfterOneSecond() 改为异步,我觉得这会分散代码要演示的内容。 - Matthew Watson

7

在你的外部代码中,你仅仅检查启动任务是否抛出异常,而不是任务本身的主体。它是异步运行的,并且启动它的代码执行完毕后就结束了。

你可以使用以下代码:

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 包装它们。


7

您可以等待,然后异常会冒泡到当前同步上下文(请参见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());

3
当选项“仅限我的代码”被启用时,Visual Studio在某些情况下会在抛出异常的那一行中断,并显示一个错误消息,其中包含以下内容:
“异常未被用户代码处理”
这个错误是无害的。你可以按下F5继续并查看这些示例中演示的异常处理行为。为了防止Visual Studio在第一个错误上中断,只需在“工具 > 选项 > 调试 > 通用”下禁用“仅限我的代码”复选框即可。

这就是我想要的。调试器停在 ThrowIfCancellationRequested() 上,而不是在调用方法中捕获它。 - Mike Lowery

0

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

我的(奇怪的?)解决方案是还要有一个Form.Timer在运行。我的Task.Run有它的队列(用于长时间运行的非UI任务),而我的Form.Timer有它的队列(用于UI任务)。

由于这种方法已经适用于我,所以添加错误处理非常简单:如果task.Run出现错误,它会将错误信息添加到Form.Timer队列中,然后显示错误对话框。


-2

在 @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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接