通过在取消令牌上注册回调来取消多个任务

时间:2019-05-16 12:21:24

标签: c# asynchronous cancellation cancellationtokensource cancellation-token

我有以下代码,输出如下。 我期待第二个任务被取消,因为它还会在取消令牌上注册一个回调。 但是取消仅在完成原始取消的第一个任务上发生。 取消不应该传播到所有令牌实例吗? Microsoft article on Cancellation Tokens不能很好地解释这一点。

关于为什么会发生这种情况的任何指示?

代码:

const x_axis = d3.axisBottom()
                 .scale(x);

输出:

    const x_axis = d3.axisBottom()
                     .scale(x)
                     .ticks(5);

3 个答案:

答案 0 :(得分:2)

失败的不仅仅是第二项任务。令牌的两个注册均有效,并且ThrowIfCancellationRequested均有效,但是由于它们在不同的线程中运行,因此无法处理。

这在后台发生(两次):

  

mscorlib.dll中发生了'System.OperationCanceledException'类型的异常,但未在用户代码中处理

您应该做的是在函数中调用cts.Token.ThrowIfCancellationRequested();而不是注册事件。

请参见https://docs.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads

上的示例

现在,您将结合两种取消方式:注册到令牌取消事件(Token.Register)和如果令牌被取消则抛出(Token.ThrowIfCancellationRequested)。

要么订阅cancel事件并执行自己的cancel / cleanup逻辑,要么检查功能码是否应该取消操作。

示例如下:

private static async Task CreateTask2(CancellationToken token)
{
    try
    {
        // Pass on the token when calling other functions.
        await Task.Delay(8000, token);

        // And manually check during long operations.
        for (int i = 0; i < 10000; i++)
        {
            // Do we need to cancel?
            token.ThrowIfCancellationRequested();

            // Simulating work.
            Thread.SpinWait(5000);
        }

        Console.WriteLine("This is task two");
    }
    catch (Exception e)
    {
        Console.WriteLine("Task 2 was cancelled by Task 1");
        Console.WriteLine(e);
    }
}

答案 1 :(得分:1)

第一件事是,当您调用CancellationToken.Register时,通常所做的只是存储委托以便以后调用。

调用CancellationTokenSource.Cancel的线程/逻辑流运行所有先前注册的委托,无论这些代理从何处注册。这意味着在这些异常中抛出的任何异常通常都与称为 Register 的方法没有任何关系。

旁注1:我在上面通常说 ,因为在某些情况下,对Register的调用将立即使委托运行。我认为这就是msdn文档特别令人困惑的原因。具体来说:如果令牌已被取消,则Register将立即运行委托,而不是存储它以便以后运行。在CancellationTokenSource.InternalRegister下发生的情况。

完成图片的第二件事是,CancellationToken.ThrowIfCancellationRequested所做的所有事情都是在从何处运行时引发异常。通常无论从何处调用CancellationTokenSource.Cancel。请注意,通常所有注册的委托都已运行,即使其中一些抛出异常。

旁注2:抛出ThreadAbortException会改变Cancel方法中的预期逻辑,因为无法捕获该特殊异常。面对这种情况,cancel停止运行任何其他委托。即使捕获异常,调用代码也会发生同样的情况。

最后要注意的是,CancellationToken的存在不会影响方法的逻辑流程。除非有代码明确地退出该方法(例如,通过引发异常),否则该方法中的所有行都将运行。如果将取消令牌传递给Task.Delay调用,并且在时间流逝之前从其他地方取消,则会发生这种情况。如果您在方法中的特定行之后向CancellationToken.ThrowIfCancellationRequested进行调用,也会发生这种情况。

答案 2 :(得分:0)

Register注册代表只是一种通知令牌何时进入取消状态的方法,仅此而已。为了执行取消操作,您需要对代码中的此通知做出反应,而当您要取消执行时,则最需要它进行到未验证取消令牌的阶段(例如,因为正在执行的方法只是不接受CancellationToken作为参数),但您仍需要对取消状态进行一些控制。但是在所有情况下,当您处理有权访问CancellationToken的代码执行时,您都不需要订阅取消通知。

在您的情况下,第一个委托引发异常,并且此异常仅传播到Cancel调用,这就是为什么任务被取消的原因,但这是不正确的设计,因为您不应该在其中处理CancellationTokenSource您的任务,不应该在那里取消,所以我说第一次取消仅是巧合。对于第二个任务,委托被调用,但是没有任何触发任务内部的取消,那么为什么要取消它呢?