【问题标题】:Passing cancellation token to Task.Run seems to have no effect [duplicate]将取消令牌传递给 Task.Run 似乎没有效果 [重复]
【发布时间】:2022-12-24 04:40:33
【问题描述】:

根据thisthis,将取消令牌传递给任务构造函数或Task.Run,将导致任务与所述令牌相关联,导致任务转换为Canceled而不是Faulted如果发生取消异常。

一段时间以来,我一直在摆弄这些示例,除了阻止已取消的任务启动之外,我看不到任何好处。

更改this MSDN example上的代码

tc = Task.Run(() => DoSomeWork(i, token), token);

tc = Task.Run(() => DoSomeWork(i, token));

产生了完全相同的输出:

此代码还会导致两个已取消的状态任务并抛出相同的异常:

var token = cts.Token;

var t1 = Task.Run(() =>
{
    while (true)
    {
        Thread.Sleep(1000);
        token.ThrowIfCancellationRequested();
    };
});

var t2 = Task.Run(() =>
{
    while (true)
    {
        Thread.Sleep(1000);
        token.ThrowIfCancellationRequested();
    };
}, token);

Console.ReadKey();

try
{
    cts.Cancel();
    Task.WaitAll(t1, t2);
}
catch(Exception e)
{
    if (e is AggregateException)
    {
        foreach (var ex in (e as AggregateException).InnerExceptions)
        {
            Console.WriteLine(e.Message);
        }
    }
    else
        Console.WriteLine(e.Message);
            
}

Console.WriteLine($"without token: { t1.Status }");
Console.WriteLine($"with token: { t2.Status }");
Console.WriteLine("Done.");

显然,从任务中抛出 OperationCanceledException 足以使其转换为 Canceled 而不是 Faulted。所以我的问题是:除了阻止已取消的任务运行之外,是否还有其他原因将令牌传递给任务?

【问题讨论】:

  • 我敢肯定,当您从取消的令牌开始时,您应该会看到不同之处。
  • 它有,如果你阅读我的问题,你会注意到我链接了最相关的。
  • “阻止取消的任务开始”正是我认为的重点,我不明白为什么你需要另一个理由
  • 因为与此问题相关的每个答案都指出有两个原因,另一个是阻止任务进入故障状态,但似乎不再以这种方式工作。
  • @DavidL AFAICS linked answer 没有解释为什么未通过 cancellationTokent1 最终处于 Canceled 状态。

标签: c# task cancellation-token


【解决方案1】:

除了阻止已取消的任务运行之外,是否有将令牌传递给任务的原因?

在这种特殊情况下,否。在任务开始运行的时间点之后,token 对结果没有影响。

Task.Run 方法有很多重载。由于无限的while 循环,这种情况很特殊。

var t1 = Task.Run(() =>
{
    while (true)
    {
        Thread.Sleep(1000);
        token.ThrowIfCancellationRequested();
    };
});

编译器必须在这两个重载之间进行选择:

public static Task Run(Action action);

public static Task Run(Func<Task> function);

...出于某种我不知道的原因,它选择了后者。 Here 是这个重载的实现:

public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
    if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);

    // Short-circuit if we are given a pre-canceled token
    if (cancellationToken.IsCancellationRequested)
        return Task.FromCanceled(cancellationToken);

    // Kick off initial Task, which will call the user-supplied function and yield a Task.
    Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
        TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

    // Create a promise-style Task to be used as a proxy for the operation
    // Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
    // as faults from task1, to support in-delegate cancellation.
    UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
        lookForOce: true);

    return promise;
}

重要的细节是lookForOce: true。让我们看看insideUnwrapPromise类:

// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;

..和下面的another point

case TaskStatus.Faulted:
    List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
    ExceptionDispatchInfo oceEdi;
    if (lookForOce && edis.Count > 0 &&
        (oceEdi = edis[0]) != null &&
        oceEdi.SourceException is OperationCanceledException oce)
    {
        result = TrySetCanceled(oce.CancellationToken, oceEdi);
    }
    else
    {
        result = TrySetException(edis);
    }
    break;

因此,尽管内部创建的 Task&lt;Task?&gt; task1 最终处于 Faulted 状态,但其解包版本最终为 Canceled,因为异常类型是 OperationCanceledException(代码中缩写为oce)。

在 TPL 的历史上,这是一个相当复杂的旅程,在不同的时间和框架中引入了方法,以服务于不同的目的。最终结果是有点不一致,或者如果你愿意这样说的话,行为会有些细微差别。您可能会感兴趣的相关文章是:Task.Run vs Task.Factory.StartNew Stephen Toub。

【讨论】:

  • 哦,我明白了......我没有抓住有关所选超载的细节,我直接进入了 Run(Action action) 一个。当强制代码实际使用它时,最终状态确实是 Faulted。所以你找到了罪魁祸首。这实际上是新信息,包括之前在这里提出的所有类似问题。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-09
相关资源
最近更新 更多