[英]TPL task continuation: task is in state Faulted instead of Canceled when cancelled
取消以下任务时,该任务不在 state 已取消但出现故障:
private string ReturnString()
{
// throw new OperationCanceledException(_cancellationToken); // This puts task in faulted, not canceled
Task.Delay(5000, _cancellationToken).Wait(_cancellationToken); // Simulate work (with IO-bound call)
// throw new OperationCanceledException(_cancellationToken); // This puts task in faulted, not canceled
// _cancellationToken.ThrowIfCancellationRequested(); // This puts task in faulted, not canceled
// throw new Exception("Throwing this exception works!"); // This works as expected (faulted)
return "Ready";
}
private void SetReturnValueWithTaskContinuation()
{
SynchronizationContext synchronizationContext = SynchronizationContext.Current;
Task<string> task = Task.Run(() => ReturnString());
task.ContinueWith(
antecedent =>
{
if (antecedent.Status == TaskStatus.Canceled)
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, "Cancelled");
}
else if (antecedent.Status == TaskStatus.Faulted)
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, "Exception");
}
else
{
synchronizationContext.Post(result => _txtResultContinueWith.Text = (string)result, antecedent.Result);
}
});
}
我知道,在抛出 OperationCanceled 异常时必须提供取消令牌。 我知道,有两种方法可以抛出 OperationCanceled 异常,其中 ThrowIfCancellationRequested() 是首选方法。 而且我知道,延续链的取消令牌应该与要取消的任务的取消令牌不同,否则延续链也会被取消。 为了简单起见,我只使用一个取消令牌来取消任务本身。 但是,该任务有 state“故障”而不是“取消”。 那是一个错误吗? 如果不是,那就是 TPL 的可用性问题。 有人可以帮忙吗?
Task.Run
确实希望我们为取消状态的正确传播提供取消令牌,请参阅:
CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态
如果我们使用接受Action
的Task.Run
或Func<T>
委托(其中T
不是Task
)的覆盖,这一点尤其重要。 如果没有令牌,在这种情况下,返回的任务状态将是Faulted
而不是Canceled
。
但是,如果委托类型是Func<Task>
或Func<Task<T>>
(例如, async
lambda),则Task.Run
会对其进行特殊处理。 委托返回的任务被解包,并且它的取消状态被正确传播。 因此,如果我们像下面这样修改您的ReturnString
,您将按预期获得Canceled
状态,并且您不必将令牌传递给Task.Run
:
private Task<string> ReturnString()
{
Task.Delay(5000, _cancellationToken).Wait(_cancellationToken);
return Task.FromResult("Ready");
}
// ...
Task<string> task = Task.Run(() => ReturnString()); // Canceled status gets propagated
如果对Task.Run
以这种方式工作的原因感到好奇,您可以深入了解它的实现细节。
但请注意,虽然从Task.Run
4.5 中引入 Task.Run 到 .NET Core 3.0 的当前版本,这种行为一直是一致的,但它仍然是未记录的并且是特定于实现的,所以我们不应该依赖它。 例如,使用Task.Factory.StartNew
仍然会产生Faulted
:
Task<string> task = Task.Factory.StartNew(() => ReturnString(),
CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current).Unwrap();
除非您关联取消令牌,否则Task.Factory.StartNew
将在此处返回Canceled
任务的唯一时间是ReturnString
已被修改为async
。 不知何故,编译器生成的async
state 机器管道改变了ReturnString
返回的Task
的行为。
通常,最好向Task.Run
或Task.Factory.StartNew
提供取消令牌。 在您的情况下,如果这是不可能的,您可能希望将ReturnString
设为async
方法:
private async Task<string> ReturnString()
{
var task = Task.Run(() =>
{
Thread.Sleep(1500); // CPU-bound work
_cancellationToken.ThrowIfCancellationRequested();
});
await task; // Faulted status for this task
// but the task returned to the caller of ReturnString
// will be of Canceled status,
// thanks to the compiler-generated async plumbing magic
return "Ready";
}
通常, 异步同步不是一个好主意,但ReturnString
是一个私有方法,它实现一些 GUI 逻辑以将工作卸载到池线程,这可能是 go 的方式。
现在,如果您想将其从当前同步上下文中删除,您可能只需要用另一个Task.Run
包装它(即使您这样做,取消状态仍将正确传播):
Task<string> task = Task.Run(() => ReturnString());
附带说明一下,不担心同步上下文的常见模式是在任何地方常规使用ConfigureAwait
:
await Task.Run(...).ConfigureAwait(continueOnCapturedContext: false);
但我自己已经无意识地停止使用ConfigureAwait
,原因如下:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.