我正在尝试了解
CancellationToken.ThrowIfCancellationRequested()
的操作。我相信它在一定程度上有效,但我只是想看看这是否是其正确的使用模式。
我有一个带有 2 个按钮的 Windows 窗体 - 一个用于启动任务,一个用于取消任务。我的代码是:
public partial class CancelForm : Form
{
private CancellationTokenSource _cancellationTokenSource;
public CancelForm()
{
InitializeComponent();
}
private async void btnStart_Click(object sender, EventArgs e)
{
_cancellationTokenSource = new CancellationTokenSource();
_cancellationTokenSource.Token.ThrowIfCancellationRequested();
await Task.Run(ThingDoingTask);
if (_cancellationTokenSource.IsCancellationRequested)
{
MessageBox.Show("Task Cancelled.");
}
else
{
MessageBox.Show("Task completed!");
}
}
private void ThingDoingTask()
{
var task = DoSomethingThatTakesALongTime();
try
{
task.Wait(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("The task was cancelled");
}
}
private async Task DoSomethingThatTakesALongTime()
{
Console.WriteLine("Doing something that takes a long time");
for (int i = 0; i < 10; i++)
{
await Task.Delay(2000);
Console.WriteLine($"Count: {i}");
}
Console.WriteLine("Finished doing it");
}
private void btnCancel_Click(object sender, EventArgs e)
{
_cancellationTokenSource.Cancel();
}
}
现在这可以工作了,当我按
btnCancel
时,它会捕获异常,然后弹出“任务已取消”消息框。
然而,这似乎非常不优雅 - 首先,我必须执行
Task.Run(ThingDoingTask)
来创建一个新线程,该线程本身调用我真正想要使用 task.Wait()
执行的操作,以将取消令牌传递给它。这是因为 task.Wait()
被阻止,所以如果我只是从 btnStart_Click()
拨打电话,我无法按“取消”按钮。 (即使 Task.Run()
确实接受了令牌,但当我取消时它也不会抛出异常。)然后我必须在完成后仔细检查 IsCancellationRequested
,因为它不知道是否抛出了异常。
其次,即使抛出异常,
DoSomethingThatTakesALongTime()
方法也会继续执行。我想我也可以在这里检查IsCancellationRequested
,但我认为ThrowIfCancellationRequested()
的目的是避免在代码中加入大量的令牌检查?并且令牌实际上在此方法中不可用(它恰好是这个简单演示类的私有属性 - 但这在复杂的应用程序中不是很有用)
编写简单应用程序或使用
ThrowIfCancellationRequested
的正确方法是什么?
这里有多个层面的“我不知道你为什么这么做”。
DoSomethingThatTakesALongTime
是 async
,但是您在 ThingDoingTask
中同步阻塞,然后为了避免阻塞您在 btnStart_Click
的 ThreadPool 上运行的 UI 线程,然后等待。
CancellationTokens 的特点在于它们是合作性的:执行长时间运行工作的事物需要定期检查令牌以查看它是否已被取消。
使用
token.ThrowIfCancellationRequested()
是执行此操作的有用方法。这会抛出一个 OperationCanceledException
,这意味着 1) 你的方法退出,你不需要担心它,2) 你的调用者知道你被取消了,因为你抛出了一个 OperationCanceledException
。
(请注意,
CancellationToken.IsCancellationRequested
在实践中几乎从未实际使用过。)
就您而言,您需要的是:
public partial class CancelForm : Form
{
private CancellationTokenSource _cancellationTokenSource;
public CancelForm()
{
InitializeComponent();
}
private async void btnStart_Click(object sender, EventArgs e)
{
_cancellationTokenSource = new CancellationTokenSource();
try
{
await DoSomethingThatTakesALongTimeAsync(_cancellationTokenSource.Token)
MessageBox.Show("Task completed!");
}
catch (OperationCanceledException)
{
MessageBox.Show("Task Cancelled.");
}
}
private async Task DoSomethingThatTakesALongTimeAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Doing something that takes a long time");
for (int i = 0; i < 10; i++)
{
await Task.Delay(2000);
Console.WriteLine($"Count: {i}");
cancellationToken.ThrowIfCancellationRequested();
}
Console.WriteLine("Finished doing it");
}
private void btnCancel_Click(object sender, EventArgs e)
{
_cancellationTokenSource.Cancel();
}
}
注意我如何将 CancellationToken 传递到
DoSomethingThatTakesALongTimeAsync
(关注点分离),以及如何在每个循环中检查它?
(事实上,这是次优的:如果令牌被取消,那么
Task.Delay
将不会被取消,所以我们可能需要最多2秒的时间来取消。你真的应该将令牌传递给Task.Delay
,这将导致它停止并立即抛出一个 OperationCanceledException
如果你这样做,那么之后就不需要立即再次检查令牌,因为它更接近地反映了 actual 。运行工作)。