如何确定CancellationTokenSource的范围?
如何确定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
方法中的三行被注释的代码,以及Cancel
和CancelAsync
方法。我的直觉告诉我,在Cancel
方法中应该有一个锁机制,而不是在Process
方法中。根据CancellationToken
来自何处,这个实现中是否存在潜在的死锁问题?不知怎么的,我对do/while
的阻塞机制不太放心。
欢迎提出任何想法。附加问题:由于CancellationToken
是一个readonly struct
并且是按值传递的,那么为什么在CancellationTokenSource
上调用Cancel
会修改CancellationToken.IsCancellationRequested
属性?也许这一直是造成困惑的根源。
如何确定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是一个有状态的类。