CancellationToken 行为

CancellationToken Behavior

我想了解 CancellationToken 的工作原理,以及它如何取消任务。

为此,我创建了这个示例,它对 Task.Run() 和内部方法使用相同的标记 - HelpDoingSomething()

所以,我让任务运行500ms,然后我取消token,结果是:

第一条打印消息:“HelpDoingSomething cancelled”,然后是 “DoSomething cancelled”

class Program
{
    static void Main(string[] args)
    {
        var cts = new CancellationTokenSource();

        var myClass = new MyClass(cts.Token);
        myClass.DoSomething();

        Thread.Sleep(500);
        cts.Cancel();

        System.Console.ReadKey();
    }
}

internal class MyClass
{
    private CancellationToken token;

    public MyClass(CancellationToken token)
    {
        this.token = token;
    }

    public void DoSomething()
    {
        Task.Run(() =>
        {
            HelpDoingSomething();
            System.Console.WriteLine("DoSomething cancelled");
        }, token);
    }

    private void HelpDoingSomething()
    {
        while (!token.IsCancellationRequested)
        {
            //Keep doing something
            System.Console.Write(".");
        }

        System.Console.WriteLine("HelpDoingSomething cancelled");
    }
}

我确切地知道我的方法 HelpDoingSomething() 检查是否在循环的每次迭代中请求了 cancellationToken。

我的问题是 Task.Run() 方法如何以及多久检查一次是否请求了取消令牌?

是否有可能 Task.Run() 会在 HelpDoingSomething() 之前进行检查,并且我只会看到打印的一条消息(“DoSomething cancelled”)?这意味着在此方法中可能无法正确处理逻辑。

传递给 Task.Run() 的取消标记在两个地方被检查:

(1) 任务真正开始前

如果在任务开始之前发出取消标记信号,任务将很快进入“WaitingToRun”状态,随后进入“Cancelled”状态。

在这种情况下将不会启动传递给任务的操作。

如果在调用 Task.Run() 后很快检查任务状态,则可能会观察到“WaitingToRun”状态,但这将是一个竞争条件,可能无法观察到该状态。但是,“已取消”状态最终总会被设置。

(2) 当传递给任务的动作抛出OperationCancelledException.

与情况 (1) 一样,任务将短暂地转换到可以观察到的“WaitingToRun”状态,但与情况 (1) 不同的是,紧随其后的是“运行”状态(假设取消令牌在任务开始后被取消)。

如果与 OperationCancelledException 关联的令牌与传递给 Task.Run() 的令牌相同,任务将转换为“已取消”状态,否则将转换为“故障”状态.

详情请看这里:https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-cancellation

这是一个示例控制台应用程序,它演示了其中的一些情况:

using System;
using System.Threading;
using System.Threading.Tasks;

static class Program
{
    public static void Main()
    {
        Console.WriteLine("Test with already-cancelled token, but not passed to Task.Run()");
        CancellationTokenSource alreadyCancelled = new CancellationTokenSource();
        alreadyCancelled.Cancel();

        Task test1 = Task.Run(() => Test(alreadyCancelled.Token));

        Console.WriteLine("test1 status: " + test1.Status); // Probably "WaitingToRun", but this has a race condition.
        Thread.Sleep(200);
        Console.WriteLine("test1 status: " + test1.Status); // Certainly "Faulted".

        Console.WriteLine("\nTest with already-cancelled token passed to Task.Run()");
        Task test2 = Task.Run(() => Test(alreadyCancelled.Token), alreadyCancelled.Token);

        Console.WriteLine("test2 status: " + test2.Status); // Probably "WaitingToRun", but this has a race condition.
        Thread.Sleep(200);
        Console.WriteLine("test2 status: " + test2.Status); // Certainly "Cancelled".

        Console.WriteLine("\nTest with token cancelled after starting task, but not passed to Task.Run()");
        CancellationTokenSource cts3 = new CancellationTokenSource();
        Task test3 = Task.Run(() => Test(cts3.Token));

        Console.WriteLine("test3 status: " + test3.Status); // Probably "WaitingToRun", but this has a race condition.
        Thread.Sleep(200);
        Console.WriteLine("test3 status: " + test3.Status); // Certainly "Running".

        cts3.Cancel();
        Thread.Sleep(200);
        Console.WriteLine("test3 status: " + test3.Status); // Certainly "Faulted".

        Console.WriteLine("\nTest with token cancelled after starting task passed to Task.Run()");
        CancellationTokenSource cts4 = new CancellationTokenSource();
        Task test4 = Task.Run(() => Test(cts4.Token), cts4.Token);

        Console.WriteLine("test4 status: " + test4.Status); // Probably "WaitingToRun", but this has a race condition.
        Thread.Sleep(200);
        Console.WriteLine("test4 status: " + test4.Status); // Certainly "Running".

        cts4.Cancel();
        Thread.Sleep(200);
        Console.WriteLine("test4 status: " + test4.Status); // Certainly "Cancelled".

        Console.ReadLine();
    }

    public static void Test(CancellationToken cancellation)
    {
        Console.WriteLine("Entering Test()");
        cancellation.WaitHandle.WaitOne();
        Console.WriteLine("Cancellation detected");
        cancellation.ThrowIfCancellationRequested();
    }
}

(评论里说“当然”有点假,但正常情况下200ms应该远远够观察到那个状态的转换了。如果不这样做,增加超时时间. 这不是在生产代码中要做的事情!)

这个输出是:

Test with already-cancelled token, but not passed to Task.Run()
test1 status: WaitingToRun
Entering Test()
Cancellation detected
test1 status: Faulted

Test with already-cancelled token passed to Task.Run()
test2 status: WaitingToRun
test2 status: Canceled

Test with token cancelled after starting task, but not passed to Task.Run()
test3 status: WaitingToRun
Entering Test()
test3 status: Running
Cancellation detected
test3 status: Faulted

Test with token cancelled after starting task passed to Task.Run()
test4 status: WaitingToRun
Entering Test()
test4 status: Running
Cancellation detected
test4 status: Canceled

请注意第二种情况“使用 already-cancelled 令牌进行测试传递给 Task.Run()”,Entering Test() 未写入控制台,因为未调用该操作。这是唯一一个根本没有调用操作的测试用例。