为什么线程在调用取消后继续 运行?

Why threads continue to run after a cancel has been called?

考虑这个简单的例子code:

var cts = new CancellationTokenSource();
var items = Enumerable.Range(1, 20);

var results = items.AsParallel().WithCancellation(cts.Token).Select(i =>
{
    double result = Math.Log10(i);                
    return result;
});

try
{
    foreach (var result in results)
    {
        if (result > 1)
            cts.Cancel();
        Console.WriteLine($"result = {result}");
    }
}
catch (OperationCanceledException e)
{
    if (cts.IsCancellationRequested)
        Console.WriteLine($"Canceled");
}

对于并行结果中的每个结果,它将打印结果直到 result > 1

此代码输出类似于:

result = 0.9030899869919435
result = 0.8450980400142568
result = 0.7781512503836436
result = 0
result = 0.6020599913279624
result = 0.47712125471966244
result = 0.3010299956639812
result = 0.6989700043360189
result = 0.9542425094393249
result = 1
result = 1.0413926851582251 <-- This is normal
result = 1.2041199826559248 <-- Why it prints this value (and below)
result = 1.0791812460476249
result = 1.2304489213782739
result = 1.1139433523068367
result = 1.255272505103306
result = 1.146128035678238
result = 1.2787536009528289
result = 1.1760912590556813
result = 1.3010299956639813
Canceled

我的问题是为什么它继续打印超过 1 的值? 我曾预计 Cancel() 令牌将终止该过程。


更新 1

建议的回答:

It's also useful to check a cancellation token inside a loop (as a means to abort the loop) or before a long operation.

我试过添加支票

foreach (var result in results)
{
    if (result > 1)
        cts.Cancel();

    if (!cts.IsCancellationRequested) //<----Check the cancellation token before printing
        Console.WriteLine($"result = {result}");
}

它仍然给出相同的结果输出。

取消令牌上的 Cancel() 只是发出取消令牌的信号,这只会影响检查令牌的代码中的其他地方(例如对 cts.IsCancellationRequested 的调用)。框架调用通常会检查取消令牌并中止。在循环内(作为中止循环的一种方式)或在长时间操作之前检查取消标记也很有用。

取消令牌不会强制终止线程或进程。还有其他 API,例如 Environment.Exit

My question is why it continue printing values over 1?

假设您雇用了一百名飞行员从一百个机场驾驶一百架飞机。他们中的一群人起飞了,然后你发了一条消息说 "cancel all the flights"。好吧,当您发送该消息时,运行路上有一堆飞机以起飞速度,消息到达后它们在空中。这些航班不会取消!

您正在发现有关多线程编程的最重要的知识。 你必须推理好像每一种可能发生的事情的顺序都可能发生。这包括比您认为应该晚到达的邮件。

特别是,您的问题是您滥用并行化机制的结果,该机制旨在并行化 长时间工作 您创建了一堆任务,运行 比发送消息停止它们所花的时间更少。在这种情况下,有些任务不足为奇任务在被告知停止后完成。

I expected that calling Cancel() on the token would terminate the process.

你的期望是完全错误的。停止期望,因为这种期望绝不符合现实。取消令牌是在方便时尽快取消操作的请求。它不会终止线程或进程。

但是,即使您确实 终止了线程,您仍然会观察到这种行为。线程终止是一个与任何其他事件一样的事件,并且该事件不是瞬时的。执行需要时间,其他线程可以在该线程终止执行时继续工作。

what do you mean by "convenient" in "a request to cancel an operation as soon as it is convenient to do so"?

让我们退一步。

如果要完成的工作非常短,那么就没有必要将其表示为任务。只做工作!一般来说,如果工作时间少于 30 毫秒,就开始工作。

因此,我们假设每个任务 需要很长时间

现在,为什么一项任务可能需要很长时间?一般有两个原因:

  • 我们正在等待另一个系统完成一些任务。我们正在等待网络数据包或磁盘读取或类似的东西。

  • 我们的计算量很大,CPU饱和了

假设我们处于第一种情况。 并行化有帮助吗?没有。如果您正在等待邮件中的包裹,雇用一个、两个、十个或一百个人来等待并不能使包裹来更快

但是确实对第二种情况有帮助;如果我们在机器中有一个额外的 CPU,我们可以用 两个 CPU 来解决问题,大约一半的时间。

因此我们可以假设 如果我们正在并行化一个任务,那是因为 CPU 做了很多工作

太棒了。现在,"CPU does a lot of work?" 的本质是什么它几乎总是在某处涉及 循环

那么,我们如何取消任务呢?我们不会通过 终止线程 来取消任务。我们要求 任务自行取消。一个设计良好的任务将采用取消标记,在其循环中 将检查取消标记是否表示任务已取消。 合作取消。该任务必须合作决定何时检查是否被取消

请注意,检查您是否被取消工作,这是占用时间的工作来自真实任务。如果你花一半的时间检查你是否被取消,你的任务就会花费两倍的时间。请记住,并行化任务的目的是使其花费一半的时间,因此加倍完成任务所需的时间是一个非首发。

因此大多数任务不会每次通过循环检查它们是否被取消。一个设计良好的任务将每隔几毫秒检查一次,而不是每隔几纳秒检查一次。

这就是我所说的 "a cancellation is a request to stop when it is convenient" 的意思。如果任务编写正确,它应该知道检查取消的好时机,以便它平衡响应性和性能。

跟进 Eric 的出色回答..."a thread or process" 和 "a unit of work" 通常不应该是一回事。创建一个线程来执行一个工作单元然后消亡就像将燃烧的箭射向空中:您无法控制它,无法预测它,并且这些箭开始相互干扰。系统因太多工作而变得窒息,无法处理任何事情。 (一个条件叫做"thrashing."

一个更好的策略是模仿快餐店:少数工人,每个人都有分配的任务,从队列中接受工作请求并将完成的三明治交付给另一个人。在任何时刻,任何队列都可能包含更多或更少的条目。你看不到任何工人倒下,死了。在午餐高峰时间,更多的工人在忙着做同样的工作。在一段缓慢的时间里,他们留在自己的岗位上,耐心地等待下一个订单的到来。任何特定的工作请求都可能被标记为 "cancelled," 并且工作人员会注意到这一点并做出相应的响应。根据管理控制,餐厅没有任何部分 "over-committed,",并且整个操作能够始终如一地每小时生产可预测数量的三明治。