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

Cancelling multiple tasks by registering callbacks on cancellation tokens

我有以下代码片段,输出如下。 我期待第二个任务被取消,因为它还在取消令牌上注册了一个回调。 但取消只发生在第一个任务上,即最初取消的地方。 取消不应该传播到所有令牌实例吗? Microsoft article on Cancellation Tokens 没有很好地解释这一点。

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

代码:

class Program
    {
        static void Main(string[] args)
        {
            AsyncProgramming();
            Console.ReadLine();
        }

        private static async void AsyncProgramming()
        {

            try
            {
                using (var cts = new CancellationTokenSource())
                {
                    var task2 = CreateTask2(cts);
                    var task1 = CreateTask1(cts);

                    Thread.Sleep(5000);
                    await Task.WhenAll(task2, task1);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("Both tasks over");
        }

        private static async Task CreateTask1(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() => { cts.Token.ThrowIfCancellationRequested(); });
                await Task.Delay(5000);
                Console.WriteLine("This is task one");
                cts.Cancel();
                Console.WriteLine("This should not be printed because the task was cancelled");
            }
            catch (Exception e)
            {
                Console.WriteLine("Task 1 exception: " + e.Message);
                Console.WriteLine("Task 1 was cancelled");
            }

        }

        private static async Task CreateTask2(CancellationTokenSource cts)
        {
            try
            {
                cts.Token.Register(() =>
                {
                    Console.WriteLine("Write something");
                    Thread.CurrentThread.Abort();
                    cts.Token.ThrowIfCancellationRequested();
                });
                await Task.Delay(8000);

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

输出:

This is task one
Write something
Task 1 exception: Thread was being aborted.
Task 1 was cancelled
This is task two
Thread was being aborted.
Both tasks over

取消失败的不仅仅是第二个任务。令牌的两个注册都有效并且 ThrowIfCancellationRequested 都触发了,但是它们没有被处理,因为它们 运行 在不同的线程中。

这在后台发生(两次):

An exception of type 'System.OperationCanceledException' occurred in mscorlib.dll but was not handled in user code

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

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

中的示例

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

要么您订阅取消事件并执行您自己的 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);
    }
}

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

在你的例子中,第一个委托引发了异常,这个异常被传播到 Cancel 调用,这就是任务被取消的原因,但这是不正确的设计,因为你不应该处理 CancellationTokenSource 在你的任务中,不应该在那里发起取消,所以我想说第一次取消只是巧合。对于第二个任务,委托被调用,但没有触发任务内部的取消,所以为什么要取消它?

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

thread/logic 流程调用 CancellationTokenSource.Cancel 运行 所有以前注册的代表,无论这些代表是从哪里注册的。这意味着那些通常抛出的任何异常与调用 Register.

的方法没有任何关系

旁注 1: 我在上面说 通常 ,因为有一种情况 Register 的调用将 运行代表马上。我认为这就是 msdn 文档特别混乱的原因。具体来说:如果令牌已经被取消,那么 Register 将立即 运行 委托,而不是稍后将其存储为 运行。在下面发生在 CancellationTokenSource.InternalRegister.

完成图片的第二件事是 CancellationToken.ThrowIfCancellationRequested 所做的就是在 运行 来自的任何地方抛出异常。那通常是 CancellationTokenSource.Cancel 被调用的地方。请注意,通常所有已注册的代表都是 运行,即使其中一些会引发异常。

旁注 2: 抛出 ThreadAbortException 会更改 Cancel 方法中的预期逻辑,因为无法捕获该特殊异常。面对这种情况时,取消会停止 运行ning 任何进一步的代表。即使在捕获异常时,调用代码也会发生同样的情况。

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