处理任务 continuewith 中异常的正确方式

71
请看下面的代码-
static void Main(string[] args)
{
    // Get the task.
    var task = Task.Factory.StartNew<int>(() => { return div(32, 0); });

    // For error handling.
    task.ContinueWith(t => { Console.WriteLine(t.Exception.Message); }, 
        TaskContinuationOptions.OnlyOnFaulted);

    // If it succeeded.
    task.ContinueWith(t => { Console.WriteLine(t.Result); }, 
        TaskContinuationOptions.OnlyOnRanToCompletion);
    Console.ReadKey();
    Console.WriteLine("Hello");
}

private static int div(int x, int y)
{
    if (y == 0)
    {
        throw new ArgumentException("y");
    }
    return x / y;
}

如果我在发布模式下执行代码,则输出为“发生一个或多个错误”,并且一旦我按下“Enter”键,“Hello”也会被显示。如果我在调试模式下运行代码,则输出与发布模式相同。但是,在IDE中进行调试时,当控件执行以下行时,将出现IDE异常消息(“用户代码中未处理的异常”)。
throw new ArgumentException("y"); 

如果我从那里继续,程序不会崩溃,并且显示与发布模式相同的输出。这是处理异常的正确方式吗?

考虑切换到async/await:它是一种更容易编写和阅读的语法,特别是在处理异常时。 - user180326
@AnirbanPaul,您可能需要更新问题并说明您的要求:VS2010和.Net 4.0,就像您在这里所做的一样(https://dev59.com/mWEi5IYBdhLWcg3wMZ7a#21521111)。 - noseratio - open to work
6个回答

88
您可能不需要单独处理OnlyOnFaultedOnlyOnRanToCompletion处理程序,并且您没有处理OnlyOnCanceled。请查看这个答案了解更多详情。
但是,在IDE中进行调试时,当控件执行该行时会显示一个IDE异常消息(“用户代码中未处理的异常”)。
您之所以在调试器下看到异常,是因为您可能已经在Debug/Exceptions选项(Ctrl+Alt+E)中启用了它。
如果我从那里继续,程序不会崩溃并显示与发布模式相同的输出。这是处理异常的正确方法吗?
Task操作中引发但未处理的异常不会自动重新引发。而是将其封装为Task.Exception(类型为AggregateException)以供将来观察。您可以将原始异常作为Exception.InnerException访问:
Exception ex = task.Exception;
if (ex != null && ex.InnerException != null)
    ex = ex.InnerException;

要使程序在这种情况下崩溃,实际上需要观察任务操作之外的异常,例如通过引用Task.Result来实现:

static void Main(string[] args)
{
    // Get the task.
    var task = Task.Factory.StartNew<int>(() => { return div(32, 0); });

    // For error handling.
    task.ContinueWith(t => { Console.WriteLine(t.Exception.Message); }, 
        TaskContinuationOptions.OnlyOnFaulted);

    // If it succeeded.
    task.ContinueWith(t => { Console.WriteLine(t.Result); }, 
        TaskContinuationOptions.OnlyOnRanToCompletion);

    Console.ReadKey();

    Console.WriteLine("result: " + task.Result); // will crash here

    // you can also check task.Exception

    Console.WriteLine("Hello");
}

更多细节:任务和未处理异常.NET 4.5中的任务异常处理

更新以回应评论:这是我在使用.NET 4.0和VS2010的UI应用程序中执行此操作的方式:

void Button_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => 
    {
        return div(32, 0); 
    }).ContinueWith((t) =>
    {
        if (t.IsFaulted)
        {
            // faulted with exception
            Exception ex = t.Exception;
            while (ex is AggregateException && ex.InnerException != null)
                ex = ex.InnerException;
            MessageBox.Show("Error: " + ex.Message);
        }
        else if (t.IsCanceled)
        {
            // this should not happen 
            // as you don't pass a CancellationToken into your task
            MessageBox.Show("Canclled.");
        }
        else
        {
            // completed successfully
            MessageBox.Show("Result: " + t.Result);
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

只要您的目标是.NET 4.0,且您想要未观察到的异常的.NET 4.0行为(即在任务被垃圾回收时重新抛出),则应该在app.config明确配置。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
  <runtime>
    <ThrowUnobservedTaskExceptions enabled="true"/>
  </runtime>
</configuration>

查看此链接以获取更多详细信息:

.NET4 中的未观察到的任务异常


我正在使用VS2010和.Net 4.0。在实际场景中,我将在Winform应用程序中应用相同类型的代码。在OnlyOnFaulted部分,我打算显示错误消息并记录异常,在OnlyOnRanToCompletion部分,我打算通过CurrentSynchronizationContext更新UI。我担心的是调试时异常消息的出现。这是否意味着异常未被处理?请告诉我上述代码模式是否是处理任务异常的安全有效的方式?如果有更好的方法,请提出建议。 - Anirban Paul
1
如果你知道你有一个AggregateException,使用Flatten()方法不是更容易吗? - takrl
你的第一个示例缺少了 task = task.ContinueWith(),因为你扩展了原始任务。 - Joel Harkes
@JoelHarkes,我不想为那个上下文再加一个ContinueWithtask.Result将简单地阻塞直到完成(无论完成状态如何),对于这样一个简单的示例来说已经足够了。 - noseratio - open to work
1
@JoelHarkes,第一段代码片段的目的只是为了向OP展示如何使他的原始代码实现他想要的功能。它并不是为了展示如何正确地做,这是在第二个片段中完成的。您的编辑版本甚至无法编译,因为task = task.ContinueWith()将在那里返回一个Task而不是Task<int>,因此根本没有Task.Result可供观察。请勿编辑它,如果您喜欢,请发布自己的答案 - noseratio - open to work
显示剩余5条评论

7

你看到的是一个AggregateException。它通常会从任务中抛出,需要查看内部异常以找到具体的异常。像这样:

task.ContinueWith(t =>
{
    if (t.Exception is AggregateException) // is it an AggregateException?
    {
        var ae = t.Exception as AggregateException;

        foreach (var e in ae.InnerExceptions) // loop them and print their messages
        {
            Console.WriteLine(e.Message); // output is "y" .. because that's what you threw
        }
    }
},
TaskContinuationOptions.OnlyOnFaulted);

在实际场景中,我将在Winform应用程序中应用相同类型的代码。在OnlyOnFaulted部分,我打算显示错误消息并记录异常,在OnlyOnRanToCompletion部分,我打算通过CurrentSynchronizationContext更新UI。我担心的是调试时异常消息的出现。这是否意味着异常未被处理?请告诉我上述代码模式是否是处理任务异常的安全有效的方式?如果有更好的方法,请提出建议。 - Anirban Paul
5
t.Exception 声明为 AggregateException 类型,因此只需测试是否为空。不需要进行 as 操作。 - Drew Noakes

3

0

"One or more errors occurred" 来自任务池生成的包装异常。如果需要,使用 Console.WriteLine(t.Exception.ToString()) 打印整个异常。

IDE 可能会自动捕获所有异常,无论它们是否被处理。


0

由于您正在使用任务,因此应该获取AggregateException,它包装了执行期间发生的所有异常。您看到One or more errors occurred消息,因为这是AggregateException.ToString()方法的默认输出。

您需要使用异常实例的Handle方法。

还可以在此处查看正确处理此类异常的方法。


-5
        try
        {
            var t1 = Task.Delay(1000);

            var t2 = t1.ContinueWith(t =>
            {
                Console.WriteLine("task 2");
                throw new Exception("task 2 error");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);

            var t3 = t2.ContinueWith(_ =>
            {
                Console.WriteLine("task 3");
                return Task.Delay(1000);
            }, TaskContinuationOptions.OnlyOnRanToCompletion).Unwrap();

            // The key is to await for all tasks rather than just
            // the first or last task.
            await Task.WhenAll(t1, t2, t3);
        }
        catch (AggregateException aex)
        {
            aex.Flatten().Handle(ex =>
                {
                    // handle your exceptions here
                    Console.WriteLine(ex.Message);
                    return true;
                });
        }

1
-1 是因为你的异常永远不会被捕获,因为使用 await 将“解包” AggregateException,所以不会有 AggregateExceptions 可以捕获。 https://dev59.com/LWs05IYBdhLWcg3wR_22 - controlbox

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