如何确定CancellationTokenSource的范围?

24 浏览
0 Comments

如何确定CancellationTokenSource的范围?

我使用async/await而不是传统的线程来实现一个长时间运行的任务,该任务将从各种情况下调用,例如桌面/网络/移动设备。这个问题涉及在使用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并且是按值传递的,那么为什么在CancellationTokenSource上调用Cancel会修改CancellationToken.IsCancellationRequested属性?也许这一直是造成困惑的根源。

0
0 Comments

如何确定CancellationTokenSource的范围?

在编写具有并发任务的代码时,我们经常需要使用CancellationTokenSource来实现任务的取消。然而,在确定CancellationTokenSource的作用域时,有一些问题和困惑。

解决这个问题的方法是使用Task.WhenAny方法。可以使用该方法来等待两个任务中的任意一个完成:一个是真正想要完成的任务,另一个是代表用户不耐烦而按下ESC键或适当的移动触摸的任务。

伪代码如下:

mainTask = 设置主任务,并将令牌作为输入
userInterruptTask = 设置用户操作监视任务,并在其延续或作为其自然循环的结束(按下ESC键)时调用Cancel
当任一任务完成时,任务结束

然后,使用以下代码等待任一任务完成:

var ret = await Task.WhenAny(mainTask, userInterruptTask);

在这一点上,如果有必要,获取ret的值并根据其值采取相应的操作。Task.WhenAny方法返回一个任务,该任务代表完成的任务。对于"what is the scope"的具体答案,令牌的范围是所有可能对其进行操作的内容。TPL中的取消是100%协作的,因此所有关心设置取消或查找取消的任务都在使用中。

对于你的辅助问题,我可以理解你的困惑。我之前没有考虑过这个问题,但答案其实很简单。该属性的实现委托给了令牌源,代码如下:

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

其中,CancellationTokenSource是一个有状态的类。

0