在等待超时后使用Task.Wait抛出两次错误
这个问题似乎源于我使用Task.Wait超时的事实。问题是任务超时后抛出的异常被记录两次,或者在未记录超时之前抛出错误。我已经添加了用于尝试更好地理解场景的代码和测试。
这个测试背后的想法是我们在异常被抛出(3秒)之前强制超时发生(2秒)。在这种情况下,例外会发生什么?结果如下。永远不会报道“繁荣”例外情况。它仍然是任务中未被观察到的异常。
[MassUpdateEngine.cs]
// Package the collection of statements that need to be run as a Task.
// The Task can then be given a cancellation token and a timeout.
Task task = Task.Run(async () =>
{
try
{
Thread.Sleep(3000);
throw new Exception("boom");
// Checking whether the task was cancelled at each step in the task gives us finer grained control over when we should bail out.
token.ThrowIfCancellationRequested();
Guid id = SubmitPreview();
results.JobId = id;
token.ThrowIfCancellationRequested();
bool previewStatus = await GetPreviewStatus(id, token);
Logger.Log("Preview status: " + previewStatus);
token.ThrowIfCancellationRequested();
ExecuteUpdate(id);
token.ThrowIfCancellationRequested();
bool updateStatus = await GetUpdateStatus(id, token);
Logger.Log("Update status: " + updateStatus);
token.ThrowIfCancellationRequested();
string value = GetUpdateResults(id);
results.NewValue = value;
}
// It appears that awaited methods will throw exceptions on when cancelled.
catch (OperationCanceledException)
{
Logger.Log("***An operation was cancelled.***");
}
}, token);
task.ContinueWith(antecedent =>
{
//Logger.Log(antecedent.Exception.ToString());
throw new CustomException();
}, TaskContinuationOptions.OnlyOnFaulted);
[Program.cs]
try
{
MassUpdateEngine engine = new MassUpdateEngine();
// This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call.
//Results results = engine.Execute();
// This call simulates calling the MassUpdate.Execute method that will handle preview + update all in one "synchronous" call along with a timeout value.
// Note: PreviewProcessor and UpdateProcessor both sleep for 3 seconds each. The timeout needs to be > 6 seconds for the call to complete successfully.
int timeout = 2000;
Results results = engine.Execute(timeout);
Logger.Log("Results: " + results.NewValue);
}
catch (TimeoutException ex)
{
Logger.Log("***Timeout occurred.***");
}
catch (AggregateException ex)
{
Logger.Log("***Aggregate exception occurred.***\n" + ex.ToString());
}
catch (CustomException ex)
{
Logger.Log("A custom exception was caught and handled.\n" + ex.ToString());
}
因为没有观察到异常,因此没有适当记录,这将无效。
此信息导致以下规则:
规则#2非常难看。在这种情况下,我们如何可靠地记录异常?我们可以使用.ContinueWith / OnlyOnFaulted来记录异常(见下文)。
task.ContinueWith(antecedent =>
{
Logger.Log(antecedent.Exception.ToString());
//throw new CustomException();
}, TaskContinuationOptions.OnlyOnFaulted);
但是,如果在Wait调用超时之前发生异常,则异常将被编组回到调用线程并由全局未处理异常处理程序处理(并记录),然后将传递给.ContinueWith任务(并记录),导致同一异常的两个错误日志条目。
我必须在这里找到一些东西。任何帮助将不胜感激。
答案 0 :(得分:0)
不要从.ContinueWith投掷。从这里抛出的异常不会被封送回调用线程。这些例外仍然是未被观察到的例外,并且被有效地吃掉。
未观察到的异常的原因是因为未观察到任务(不是因为它是使用ContinueWith
创建的)。从ContinueWith
返回的任务被忽略了。
我说not use ContinueWith
at all更合适的建议(有关详情,请参阅我的博客)。一旦你改为使用await
,答案就会变得更加清晰:
public static async Task LogExceptions(Func<Task> func)
{
try
{
await func();
}
catch (Exception ex)
{
Logger.Log(ex.ToString());
}
}
public static async Task<T> LogExceptions<T>(Func<Task<T>> func)
{
try
{
return await func();
}
catch (Exception ex)
{
Logger.Log(ex.ToString());
}
}
async
/ await
模式更清楚地说明了这里真正发生的事情:代码创建的包装器任务必须用作< em>替换用于原始任务:
Task task = LogExceptions(() => Task.Run(() => ...
现在,包装器任务将始终观察其内部任务的任何异常。
当使用等待超时时,来自任务的异常可能会或可能不会被编组回调用线程。如果在Wait调用超时之前发生异常,则异常会被编组回调用线程。如果在Wait调用超时后发生异常,则异常仍然是任务的未观察异常。
我在方面考虑到这一点,其中例外是而不是&#34;编组&#34;或者&#34;调用线程&#34;。这只是混淆了这个问题。当任务发生故障时,异常将放在任务上(并且任务完成)。因此,如果在等待完成之前将异常置于任务上,则任务完成将满足等待,并引发异常。如果在任务完成之前等待超时,则等待不再等待,因此它当然不会看到异常,并且可能无法观察到异常。