ContinueWith TaskContinuationOptions.OnlyOnFaulted 似乎没有捕捉到从已启动任务中抛出的异常

ContinueWith TaskContinuationOptions.OnlyOnFaulted does not seem to catch an exception thrown from a started task

我正在尝试使用 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();    

如果我像下面这样在任务方法中处理异常,一切都会如我所料地正常进行。任务运行到完成,异常被记录并且 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 是否正确,还是在任务方法本身中处理异常总是更好?即使任务 运行s 出现异常,我也希望主线程继续。另外,我想从任务方法中记录该异常。

注意:我必须等待任务方法完成才能继续(有或没有错误)。

总结一下(目前我的理解)

如果来自 Task 的异常得到处理,即如果 wait 或 await 捕获异常,则异常将传播到 continuedtask onfaulted。 即使在任务方法和 consumed\handled 中也可以捕获异常。

try
{ 
   t.wait();
}
catch(Exception e)
{
   LogError(e);
}

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

看来您的方向是正确的。我有 运行 你的代码并得到相同的结果。我的建议是直接从操作内部使用 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;

[编辑] 删除了外部异常处理程序。

Am I using the TaskContinutationOptions.OnlyOnFaulted correctly or is it always better to handle the exception in the task method itself? I would like the main thread to continue even if task runs into exception.

您可以在内部或外部以任何方式处理异常。这是一个偏好问题,通常取决于用例。

请注意,您没有做的一件事是保留对您的延续的引用。您在传播异常的原始任务上使用 Task.Wait,而不管您是否有处理它的延续。

让我困扰的一件事是您使用的是 Task.Wait 同步等待 而不是 await 异步等待。这就是 AggregationException 的原因。更重要的是,您不应该阻止异步操作,因为这会导致您陷入一个您可能不想去的兔子洞,并出现各种同步上下文问题。

我个人会做的是在 ContinueWith 中使用 await,因为它的选项不那么冗长。另外,我会使用 Task.Run over 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.
}

首先,您没有正确使用 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
}

最初的问题恰好 运行 它的 taskAction 在一个单独的线程上。情况可能并非总是如此。

很好的解决了问题。如果我们不想使用单独的线程怎么办?如果我们想简单地启动一个任务,运行同步它直到我们遇到 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