如何确定 CancellationTokenSource 范围?

How to determine CancellationTokenSource scope?

我没有使用常规线程,而是使用 async/await 来实现一个长 运行 作业,该作业将从 Desktop/Web/Mobile.

等各种场景调用

这个问题是关于使用 CancellationTokenSource/CancellationToken 对象时的设计注意事项。考虑以下用 .NET Core 5 编写的代码:

System
System.Collections.Generic
System.Diagnostics
System.IO
System.Threading
System.Threading.Tasks

[STAThread]
private static async Task Main ()
{
    using (var job = new Job())
    //using (var source = new CancellationTokenSource())
    {
        var watch = Stopwatch.StartNew();

        job.OnJobProgress += (sender, e) => { Console.WriteLine (watch.Elapsed); };

        Task.Run (async () => await job.StartAsync());
        //Task.Run (async () => await job.StartAsync (source.Token));

        do
        {
            await Task.Delay (100);
            if ((Console.KeyAvailable) && (Console.ReadKey ().Key == ConsoleKey.Escape))
            {
                //source.Cancel();
                await job.CancelAsync();
                break;
            }
        }
        while (job.Running);
    }
}

public class Job : IDisposable
{
    public EventHandler OnJobProgress;

    private bool _Running = false;
    private readonly object SyncRoot = new object();
    private CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();

    public bool Running => this._Running;

    public async Task StartAsync () => await this.StartAsync(CancellationToken.None);
    public async Task StartAsync (CancellationToken cancellationToken) => await this.ProcessAsync(cancellationToken);

    public void Cancel ()
    {
        this.CancellationTokenSource?.Cancel();
        do { Thread.Sleep (10); } while (this._Running);
    }

    public async Task CancelAsync ()
    {
        this.CancellationTokenSource?.Cancel();
        do { await Task.Delay (10); } while (this._Running);
    }

    private async Task ProcessAsync (CancellationToken cancellationToken)
    {
        lock (this.SyncRoot)
        {
            if (this._Running) { return; }
            else { this._Running = true; }
        }

        do
        {
            await Task.Delay (100);
            this.OnJobProgress?.Invoke (this, new EventArgs());
        }
        while (!cancellationToken.IsCancellationRequested);

        lock (this.SyncRoot)
        {
            this._Running = false;
            this.CancellationTokenSource?.Dispose();
            this.CancellationTokenSource = new CancellationTokenSource();
        }
    }

    public void Dispose () => this.Cancel();
}

请注意 Main 方法以及 CancelCancelAsync 方法中的三个注释行。我的直觉告诉我应该在 Cancel 方法而不是 Process 方法中使用锁定机制。根据 CancellationToken 的来源,此实现中是否存在任何潜在的死锁?不知何故,我对 do/while 阻止机制感到不舒服。

如有任何想法,我们将不胜感激。

辅助问题: 因为 CancellationToken 是一个 readonly struct 并且在 by value 周围传递,所以调用 CancelCancellationTokenSource上修改CancellationToken.IsCancellationRequested属性?也许这就是一直以来混乱的根源。

这是 Task.WhenAny 的工作。等待第一项工作从两项中完成:要么是您真正想要完成的工作,要么是代表用户不耐烦的工作,请按 ESC 键或适当的移动触摸。

伪代码:

  • mainTask = Setup main task, take the token as input。就是这样。
  • userInterruptTask = Setup user action monitoring task,并在它的延续中或作为其自然循环结束时间的一部分(ESC 键),调用 Cancel。请注意,在此循环中,没有针对布尔值的检查;它一直持续到它必须取消,然后通过 break/return 完成;如果正确侦听取消,则其他任务将完成。
  • 因此,当任一任务完成时,您就完成了。
var ret = await Task.WhenAny(mainTask, userInterruptTask);

如果此时重要,请获取 ret 的值并采取相应的行动。 Task.WhenAnyreturns

A task that represents the completion of one of the supplied tasks. The return task's Result is the task that completed.

对于令牌“范围是什么”的具体答案...它的范围是可能作用于它的一切。 TPL 中的取消是 100% 合作的,因此所有关心设置取消或寻找取消的任务都在进行中。

对于你的辅助问题,我能理解你的困惑。我自己以前没有想到过,但事实证明答案很简单。 属性 委托给令牌源的实现:

public bool IsCancellationRequested 
    => _source != null && _source.IsCancellationRequested;

其中 CancellationTokenSource 是有状态的 class。