ContinueWith TaskContinuationOptions.OnlyOnFaulted 似乎无法捕获从已启动任务抛出的异常。

6

我正在尝试使用ContinueWith和OnlyOnFaulted捕捉从任务方法抛出的异常,如下所示。然而,在我尝试运行此代码时,我遇到了未处理的异常。

由于我已经处理了异常,因此我希望任务能够完成运行。但是Task.Wait()会遇到AggregateException。

var taskAction = new Action(() =>
{
    Thread.Sleep(1000); 
    Console.WriteLine("Task Waited for a sec");
    throw (new Exception("throwing for example"));
});
Task t = Task.Factory.StartNew(taskAction);
t.ContinueWith(x => Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  t.Exception), TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);
t.Wait();    

如果我像下面这样在任务方法中处理异常,一切都会按照我的预期正常进行。任务完成,异常被记录下来,等待也成功了。
var taskAction = new Action(() =>
{
    try
    {
        Thread.Sleep(1000); 
        Console.WriteLine("Task Waited for a sec"); 
        throw (new Exception("throwing for example"));
    }
    catch (Exception ex)
    {
        Console.WriteLine("Catching the exception in the Action catch block only");
    }
});
Task t = Task.Factory.StartNew(taskAction);
t.ContinueWith(x=> Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  t.Exception), TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);
t.Wait();    

我的问题是:我是否正确使用了OnlyOnFaulted,或者总是更好地在任务方法本身处理异常?即使任务遇到异常,我也希望主线程继续运行。此外,我还想从任务方法记录该异常。
注意:在继续进行(无论有无错误)之前,我必须等待任务方法完成。
总结一下(我目前的理解):
如果处理了来自Task的异常,即如果wait或await捕获了异常,则异常将传播到continuedtask onfaulted。 甚至可以在任务方法中捕获和消耗\处理异常。
try
{ 
   t.wait();
}
catch(Exception e)
{
   LogError(e);
}

在上述情况中,在调用LogError之前,将执行与主任务的onfaulted相关联的继续任务。

你正在使用哪个版本的.NET框架? - Yuval Itzchakov
@YuvalItzchakov 使用 .NET 4.5。 - seesharpconcepts
4个回答

19
首先,你没有正确使用OnlyOnFaulted。当你在任务上使用ContinueWith时,实际上并没有改变那个任务,而是得到了一个任务继续体(在你的情况下,你忽略了它)。
如果原始任务出现故障(即在其中抛出异常),它将保持故障状态(因此在其上调用Wait()将总是重新抛出异常)。然而,继续体将在任务发生故障后运行并处理异常。
这意味着在你的代码中,你确实处理了异常,但你也通过Wait()重新抛出了它。正确的代码应该是这样的:
Task originalTask = Task.Run(() => throw new Exception());
Task continuationTask = originalTask.ContinueWith(
    t => Console.WriteLine(t.Exception), 
    TaskContinuationOptions.OnlyOnFaulted);

continuationTask.Wait();
// Both tasks completed. No exception rethrown

现在,正如Yuval Itzchakov指出的那样,你可以在任何地方处理异常,但如果可能的话最好使用async-await以异步方式等待(在Main中无法这样做),而不是使用Wait()阻塞。
try
{
    await originalTask;
}
catch (Exception e)
{
    // handle exception
}

这就是我正在寻找的信息。我无法点赞,可能是因为我在这里还很新,并且声望不够:( - seesharpconcepts
@seesharpconcepts 而且,在达到15分之前,您无法为答案投票。更多信息请参见:http://meta.stackexchange.com/q/1661/246036 - i3arnon
如果你等待继续任务并且第一个任务中没有异常,那么这段代码将抛出一个任务取消异常。 - C.M.
在你的例子中,如果originalTask没有任何异常,你会从continuationTask得到一个相当令人困惑的TaskCanceledException。https://dotnetfiddle.net/XiD2Z0 所以我猜你仍然需要在你的继续任务中检查t.IsFaulted,并且完全放弃TaskContinuationOptions.OnlyOnFaulted,因为显然它只会使事情更加混乱,而且没有任何价值。https://dotnetfiddle.net/4ltVIs - undefined

4
我是否正确使用了TaskContinutationOptions.OnlyOnFaulted,还是在任务方法本身中处理异常总是更好?即使任务运行遇到异常,我希望主线程仍然可以继续执行。
您可以用任何一种方式处理异常,无论内部或外部都可以。这是一个偏好的问题,通常取决于使用情况。
请注意,您没有保留对延续的引用。您在原始任务上使用了Task.Wait,这会传播异常,无论您是否有处理它的延续。
有一件事困扰着我,那就是您正在使用异步等待而不是同步等待,所以出现了AggregationException。此外,您不应该阻塞异步操作,因为这将使您陷入一些您可能不想进入的困境,包括各种同步上下文问题。
个人建议是,在ContinueWith中使用await,因为这是更简洁的选项。同时,我会使用Task.Run 覆盖 Task.Factory.StartNew
var task = Task.Run(() => 
{
    Thread.Sleep(1000);
    throw new InvalidOperationException();
}

// Do more stuff here until you want to await the task.

try
{           
    await task;
}
catch (InvalidOperationException ioe)
{
    // Log.
}

感谢您的回答,特别是关于异常传播的部分。我无法为您的答案点赞(可能是因为我在这里还很新,并没有足够的声望:() - seesharpconcepts
我需要在我的情况下进行同步等待。我的意图是在继续之前完成两种方法(并行),因此我猜想在我的情况下使用await不起作用。我甚至可以使用Parallel.Invoke来执行这两种方法。 - seesharpconcepts
为什么你必须同步等待? - Yuval Itzchakov
假设我需要调用methodA()方法返回一个列表和methodB()方法返回另一个列表。我将必须调用另一个服务方法,并将输入参数设置为由methodA()和methodB()返回的列表的某种联合。在这种情况下,我可以并行调用methodA()和methodB(),但我必须等待它们都返回并形成结果列表。我将发送此列表到服务,并使用服务返回的内容形成另一个文档。这是用户交互,用户将等待操作完成。 - seesharpconcepts
1
@seesharpconcepts 为什么不直接使用 await Task.WhenAll(taskA, taskB); 呢? - Yuval Itzchakov

0

原始问题恰好在单独的线程上运行其taskAction。这可能并不总是如此。

i3arnon的答案很好地解决了这个问题。如果我们不想使用单独的线程怎么办?如果我们只想简单地启动一个任务,在遇到IO或延迟之前同步运行它,然后继续自己的业务。直到最后,我们将等待任务完成。

我们如何等待任务并重新抛出异常?我们将使用一个空的继续,当任务因任何原因完成时,它将始终成功,而不是将其包装在Task.Run()中。

// Local function that waits a moment before throwing
async Task ThrowInAMoment()
{
    await Task.Delay(1000);
    Console.WriteLine("Task waited for a sec");
    throw new Exception("Throwing for example");
}

// Start the task without waiting for it to complete
var t = ThrowInAMoment();

// We reach this line as soon as ThrowInAMoment() can no longer proceed synchronously
// This is as soon as it hits its "await Delay(1000)"

// Handle exceptions in the task
t.ContinueWith(x => Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  t.Exception), TaskContinuationOptions.OnlyOnFaulted);

// Continue about our own business
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);

// Now we want to wait for the original task to finish
// But we do not care about its exceptions, as they are already being handled
// We can use ContinueWith() to get a task that will be in the completed state regardless of HOW the original task finished (RanToCompletion, Faulted, Canceled)
await t.ContinueWith(task => {});

// Could use .Wait() instead of await if you want to wait synchronously for some reason

-1

看起来你走在了正确的轨道上。我已经运行了你的代码并得到了相同的结果。我的建议是直接从操作内部使用try/catch。这将使你的代码更清晰,并允许你记录诸如它来自哪个线程之类的信息,这在继续路径中可能会有所不同。

var taskAction = new Action(() =>
{
    try{
        Thread.Sleep(1000); 
        Console.WriteLine("Task Waited for a sec");
        throw (new Exception("throwing for example"));
    }
    catch (Exception e)
    {
        Console.WriteLine("In the on Faulted continue with code. Catched exception from the task."+  e);
    }

});
Task t = Task.Factory.StartNew(taskAction);
Console.WriteLine("Main thread waiting for 4 sec");
Thread.Sleep(4000);
Console.WriteLine("Wait of 4 secs complete..checking if task is completed?");
Console.WriteLine("Task State: " + t.Status);
await t;

[编辑] 移除外部例外处理程序。


不需要内部的try catch...如果要捕获异常,你需要在调用任务的wait或result的地方放置try catch。 - Pranay Rana
同意,但我花时间阅读了规范,“我想记录来自任务方法的异常”。 - Phillip Scott Givens
@PhillipScottGivens 我在思考为什么即使在任务方法中出现异常,ContiueWith任务的onfaulted也没有被执行。我阅读了一篇MSDN文章,建议使用ContinueWith和onlyonfaulted来处理任务中的异常。 - seesharpconcepts
当我运行您的代码时,确实执行了ContinueWith任务,但它并没有阻止异常进一步向上冒泡。请再次尝试您的代码,并在Console.Writeline上设置断点。考虑将其移动到不同的行以增加清晰度。 - Phillip Scott Givens
@PhillipScottGivens:正如I3arnon在他的答案中提到的那样,只有在处理完主任务的异常后,continuetask才会被执行。注意:我没有对你的答案进行投票,感谢你的帮助。 - seesharpconcepts
在同时运行任务时,我更喜欢使用这种方式(在任务内部处理异常并使用返回值来表示成功/失败),而不是使用OnlyOnFaulted的任务连续性。当你有许多任务并且需要知道哪个任务/连续性要等待时,控制流会变得非常混乱。 - C.M.

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