如何确定 CancellationTokenSource 范围?

Posted

技术标签:

【中文标题】如何确定 CancellationTokenSource 范围?【英文标题】:How to determine CancellationTokenSource scope? 【发布时间】:2021-07-20 04:25:27 【问题描述】:

我没有使用传统的线程,而是使用async/await 来实现一个长时间运行的作业,该作业将从桌面/Web/移动等各种场景中调用。

这个问题是关于使用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,那么在CancellationTokenSource 上调用Cancel 是如何修改CancellationToken.IsCancellationRequested 属性的?也许这一直是混乱的根源。

【问题讨论】:

Somehow, I am not comfortable with the do/while blocking mechanism - 所以不要使用它?只需在Mainawait 你的Task.Run(...),然后有一个不相关的按键处理程序来标记取消令牌? 你的直觉是正确的。 bool 标志和观察该标志的循环的这种组合效率低下,引入了延迟,而且肯定有问题。一个线程不应该在没有同步的情况下读取由另一个线程变异的非volatile 变量。当前线程可能看不到更改的值。你可以看看这个:C# and thread-safety of a bool. 【参考方案1】:

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

伪代码:

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

如果此时很重要,请获取ret 的值并采取相应措施。 Task.WhenAny returns

表示完成所提供任务之一的任务。返回任务的Result就是完成的任务。

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

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

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

CancellationTokenSource 是一个有状态的类。

【讨论】:

以上是关于如何确定 CancellationTokenSource 范围?的主要内容,如果未能解决你的问题,请参考以下文章

如何确定样本量

如何确定样本量

如何确定滚动视图中当前可见的区域并确定中心?

Flink on YARN时,如何确定TaskManager数

如何确定 div 是不是滚动到底部?

七步法计算测量不确定度:第四步