【问题标题】:How to determine CancellationTokenSource scope?如何确定 CancellationTokenSource 范围?
【发布时间】: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.

标签: c# .net .net-core async-await cancellationtokensource


【解决方案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 是一个有状态的类。

【讨论】:

    猜你喜欢
    • 2019-02-28
    • 2018-02-10
    • 1970-01-01
    • 2020-12-24
    • 1970-01-01
    • 1970-01-01
    • 2010-10-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多