简体   繁体   English

TPL 任务继续:任务在 state 中发生故障而不是取消时取消

[英]TPL task continuation: task is in state Faulted instead of Canceled when cancelled

When cancelling the following task, the task is not in state Canceled but Faulted:取消以下任务时,该任务不在 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);
            }
        });
    }

I know, that the cancellation token has to be supplied when throwing an OperationCanceled Exception.我知道,在抛出 OperationCanceled 异常时必须提供取消令牌。 I know, there are two ways of throwing an OperationCanceled Exception where the ThrowIfCancellationRequested() is the prefered one.我知道,有两种方法可以抛出 OperationCanceled 异常,其中 ThrowIfCancellationRequested() 是首选方法。 And I know, that the cancellation token of the continuation chain should be different than the cancellation token of the task to cancel, otherwise the continuation chain will be canceled too.而且我知道,延续链的取消令牌应该与要取消的任务的取消令牌不同,否则延续链也会被取消。 For the sake of simplification, I only use one cancellation token to cancel the task itself.为了简单起见,我只使用一个取消令牌来取消任务本身。 But, the task has state "Faulted" and not "Canceled".但是,该任务有 state“故障”而不是“取消”。 Is that a bug?那是一个错误吗? If not, than it is a usability issue of the TPL.如果不是,那就是 TPL 的可用性问题。 Can somebody help?有人可以帮忙吗?

Task.Run does want us to provide a cancellation token for proper propagation of the cancellation status, see: Task.Run确实希望我们为取消状态的正确传播提供取消令牌,请参阅:

Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态

This is particularly important if we use the overrides of Task.Run that accept Action , or Func<T> delegate where T is anything but Task .如果我们使用接受ActionTask.RunFunc<T>委托(其中T不是Task )的覆盖,这一点尤其重要。 Without a token, the returned task status will be Faulted rather than Canceled in this case.如果没有令牌,在这种情况下,返回的任务状态将是Faulted而不是Canceled

However, if the delegate type is Func<Task> or Func<Task<T>> (eg, an async lambda), it gets some special treatment by Task.Run .但是,如果委托类型是Func<Task>Func<Task<T>> (例如, async lambda),则Task.Run会对其进行特殊处理。 The task returned by the delegate gets unwrapped and its cancellation status is properly propagated.委托返回的任务被解包,并且它的取消状态被正确传播。 So, if we amend your ReturnString like below, you'll get the Canceled status as expected, and you don't have to pass a token to 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

If curious about why Task.Run works that way, you can dive into its implementation details .如果对Task.Run以这种方式工作的原因感到好奇,您可以深入了解它的实现细节

Note though, while this behavior has been consistent from when Task.Run was introduced in .NET 4.5 through to the current version of .NET Core 3.0, it's still undocumented and implementation-specific , so we shouldn't rely upon it.但请注意,虽然从Task.Run 4.5 中引入 Task.Run 到 .NET Core 3.0 的当前版本,这种行为一直是一致的,但它仍然是未记录的并且是特定于实现的,所以我们不应该依赖它。 For example, using Task.Factory.StartNew instead would still produce Faulted :例如,使用Task.Factory.StartNew仍然会产生Faulted

Task<string> task = Task.Factory.StartNew(() => ReturnString(),
     CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current).Unwrap();

Unless your associate a cancellation token, the only time when Task.Factory.StartNew would return a Canceled task here is when ReturnString has been modified to be async .除非您关联取消令牌,否则Task.Factory.StartNew将在此处返回Canceled任务的唯一时间是ReturnString已被修改为async Somehow, the compiler-generated async state machine plumbing changes the behavior of the Task returned by ReturnString .不知何故,编译器生成的async state 机器管道改变了ReturnString返回的Task的行为。

In general, it's always best to provide a cancellation token to Task.Run or Task.Factory.StartNew .通常,最好向Task.RunTask.Factory.StartNew提供取消令牌。 In your case, if this is not possible, you may want to make ReturnString an async method:在您的情况下,如果这是不可能的,您可能希望将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";
}

Usually, doing async-over-sync is not a good idea , but where ReturnString is a private method implementing some GUI logic to offload work to a pool thread, this is might the way to go.通常, 异步同步不是一个好主意,但ReturnString是一个私有方法,它实现一些 GUI 逻辑以将工作卸载到池线程,这可能是 go 的方式。

Now, you might only ever need to wrap this with another Task.Run if you wanted to take it off the current synchronization context (and if even you do so, the cancellation status will still be correctly propagated):现在,如果您想将其从当前同步上下文中删除,您可能只需要用另一个Task.Run包装它(即使您这样做,取消状态仍将正确传播):

Task<string> task = Task.Run(() => ReturnString());

On a side note, a common pattern to not worry about synchronization context is to routinely use ConfigureAwait everywhere:附带说明一下,不担心同步上下文的常见模式是在任何地方常规使用ConfigureAwait

await Task.Run(...).ConfigureAwait(continueOnCapturedContext: false); 

But I myself have stopped using ConfigureAwait unconsciously, for the following reason:但我自己已经无意识地停止使用ConfigureAwait ,原因如下:

Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)重新访问 Task.ConfigureAwait(continueOnCapturedContext: false)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM