如何确定 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
方法以及Cancel
和CancelAsync
方法中的三个注释行。我的直觉说应该在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
- 所以不要使用它?只需在Main
中await
你的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 范围?的主要内容,如果未能解决你的问题,请参考以下文章