【问题标题】:Why threads continue to run after a cancel has been called?为什么在调用取消后线程继续运行?
【发布时间】:2019-11-27 17:35:02
【问题描述】:

考虑这个简单的例子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

@mike-s的回答建议:

检查循环内的取消标记也很有用(作为 意味着中止循环)或在长时间操作之前。

我已尝试添加支票

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

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

它仍然给出相同的结果。

【问题讨论】:

    标签: c# linq parallel.foreach cancellation-token


    【解决方案1】:

    我的问题是为什么它继续打印超过 1 的值?

    想象一下,您聘请了一百名飞行员从一百个机场驾驶一百架飞机。一大堆起飞,然后你发一条消息说“取消所有航班”。好吧,当您发送该消息时,跑道上有一堆飞机以起飞速度,并且消息在飞行后到达。这些航班不会被取消!

    您正在发现关于多线程编程最重要的知识。 你必须假设所有可能发生的事情的顺序都可能发生。这包括比您认为应该迟到的消息。

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

    我预计在令牌上调用 Cancel() 会终止进程。

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

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

    “在方便时取消操作的请求”中的“方便”是什么意思?

    让我们退后一步。

    如果要完成的工作非常短,那么就没有必要将其表示为一项任务。干活!一般来说,如果工作花费的时间少于 30 毫秒,只需完成工作即可。

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

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

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

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

    假设我们处于第一种情况。 并行化有帮助吗?没有。如果您在等待邮件中的包裹,雇用一、二、十或一百人等待并不会让包裹更快送达 .

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

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

    太好了。现在,“CPU 做了很多工作”的本质是什么?它几乎总是在某处涉及一个循环

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

    请注意,检查您是否被取消工作,而且这项工作会占用真正的任务。如果您花一半的时间检查您是否被取消,那么您的任务将花费两倍的时间。请记住,并行化任务的目的是使其花费一半,因此加倍完成任务所需的时间是不可能的。

    因此,大多数任务不会在每次循环中检查它们是否被取消。精心设计的任务将每隔几毫秒检查一次,而不是每隔几纳秒检查一次。

    这就是我所说的“取消是在方便时请求停止”的意思。如果编写正确,该任务应该知道什么是检查取消的好时机,以便平衡响应能力和性能。

    【讨论】:

    • 嘿@eric-lippert,as soon as it is convenient to do so - 你说的方便是什么意思?什么时候会真正发生?
    • @ShaharShokrani:这取决于接受CancellationToken 的代码。此类代码可以使用IsCancellationRequestedThrowIfCancellationRequestedRegister 来停止进一步的工作。对于网络 API,在请求中停止工作是正常的。对于接受委托的 API(例如,任务并行库),它们通常会在请求下一个工作项时取消。是否方便取消部分工作项由您决定(例如,通过在工作项中调用ThrowIfCancellationRequested);第三方库无从知晓。
    【解决方案2】:

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

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

    【讨论】:

    • 嘿@mike-s 感谢您的回答,我还没有检查Environment.Exit 但请参阅更新问题。
    【解决方案3】:

    跟进 Eric 的出色回答……“线程或进程”和“工作单元”通常不应该是同一回事。创建一个线程来执行一个工作单元然后死亡就像向空中发射燃烧的箭头:你无法控制它,无法预测它,这些箭头开始相互干扰。系统因大量工作而窒息,以至于无法处理任何事情。 (一种称为“抖动”的情况。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-08-26
      • 2015-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多