如何在不抛出/捕获异常的情况下使用CancellationToken?

168
与之前的代码for class RulyCanceler相比,我想使用CancellationTokenSource来运行代码。

如何像Cancellation Tokens中所述那样使用它,即不抛出/捕获异常?我可以使用IsCancellationRequested属性吗?

我尝试像这样使用它:

cancelToken.ThrowIfCancellationRequested();

并且

try
{
  new Thread(() => Work(cancelSource.Token)).Start();
}
catch (OperationCanceledException)
{
  Console.WriteLine("Canceled!");
}

但是这个方法在Work(CancellationToken cancelToken)中的cancelToken.ThrowIfCancellationRequested();处运行时出现了一个运行时错误:

System.OperationCanceledException was unhandled
  Message=The operation was canceled.
  Source=mscorlib
  StackTrace:
       at System.Threading.CancellationToken.ThrowIfCancellationRequested()
       at _7CancellationTokens.Token.Work(CancellationToken cancelToken) in C:\xxx\Token.cs:line 33
       at _7CancellationTokens.Token.<>c__DisplayClass1.<Main>b__0() in C:\xxx\Token.cs:line 22
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException:

我成功运行的代码在新线程中捕获了OperationCanceledException异常:

using System;
using System.Threading;
namespace _7CancellationTokens
{
  internal class Token
  {
    private static void Main()
    {
      var cancelSource = new CancellationTokenSource();
      new Thread(() =>
      {
         try
         {
           Work(cancelSource.Token); //).Start();
         }
         catch (OperationCanceledException)
         {
            Console.WriteLine("Canceled!");
         }
         }).Start();

      Thread.Sleep(1000);
      cancelSource.Cancel(); // Safely cancel worker.
      Console.ReadLine();
    }
    private static void Work(CancellationToken cancelToken)
    {
      while (true)
      {
        Console.Write("345");
        cancelToken.ThrowIfCancellationRequested();
      }
    }
  }
}

2
https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads 这个网站有一些非常好的示例,展示了如何在异步方法、长时间运行的方法中使用 CancellationTokenSource,以及如何使用回调函数。 - Ehtesh Choudhury
这篇文章展示了你在处理令牌时需要考虑的选项和根据你特定情况需要采取的措施。 - Ognyan Dimitrov
4个回答

192
您可以按以下方式实现您的工作方法:
private static void Work(CancellationToken cancelToken)
{
    while (true)
    {
        if(cancelToken.IsCancellationRequested)
        {
            return;
        }
        Console.Write("345");
    }
}

这就是了。您总是需要自己处理取消 - 在适当的时间退出方法(以便您的工作和数据处于一致的状态)。
更新:我更喜欢不写while(!cancelToken.IsCancellationRequested),因为往往有几个退出点可以安全地停止执行循环体,而循环通常有一些逻辑条件来退出(迭代集合中的所有项等)。因此,我认为最好不要混淆这些条件,因为它们具有不同的意图。
关于避免CancellationToken.ThrowIfCancellationRequested()的注意事项: 评论问题Eamon Nerbonne提出:
"...将ThrowIfCancellationRequested替换为对IsCancellationRequested进行一系列检查,如本答案所述,可以优雅地退出。但这不仅仅是实现细节;它会影响可观察的行为:任务将不再以取消状态结束,而是以RanToCompletion结束。这不仅可以影响显式状态检查,还可以更微妙地影响使用例如ContinueWith的任务链接,具体取决于所使用的TaskContinuationOptions。我认为避免使用ThrowIfCancellationRequested是危险的建议。"

1
谢谢!这个结论并不是来自于在线文本,它相当有权威性(《C# 4.0 in a Nutshell》一书?)。你能给我提供一个关于“总是”的参考吗? - Fulproof
1
这是基于实践和经验的结论 =)。我已经忘记了我从哪里知道这个的来源。我使用“你总是需要”的原因是,你实际上可以使用Thread.Abort()从外部中断工作线程,但这是非常不好的做法。顺便说一句,使用CancellationToken.ThrowIfCancellationRequested()也是“自行处理取消”,只是另一种处理方式。 - Sasha
1
你能做到 while(!cancelToken.IsCancellationRequested) 吗? - Doug Dawson
2
@OleksandrPshenychnyy 我的意思是将 while(true) 替换为 while(!cancelToken.IsCancellationRequested)。这很有帮助!谢谢! - Doug Dawson
3
如果你不打算手动取消你要开始的操作,你可以使用CancellationToken.None。当然,如果系统进程被终止,一切都会中断,CancellationToken也无能为力。所以是的,你只有在确实需要使用它来取消操作时才应该创建CancellationTokenSource。没有必要创建你不使用的东西。 - Sasha
显示剩余8条评论

29

您需要将CancellationToken传递给任务,该任务会定期监视令牌以查看是否请求取消。

// CancellationTokenSource provides the token and have authority to cancel the token
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;  

// Task need to be cancelled with CancellationToken 
Task task = Task.Run(async () => {     
  while(!token.IsCancellationRequested) {
      Console.Write("*");         
      await Task.Delay(1000);
  }
}, token);

Console.WriteLine("Press enter to stop the task"); 
Console.ReadLine(); 
cancellationTokenSource.Cancel(); 

在这种情况下,当请求取消时操作将结束,Task 将处于 RanToCompletion 状态。如果你想要得到通知你的任务已被取消,你必须使用 ThrowIfCancellationRequested 来抛出一个 OperationCanceledException 异常。

Task task = Task.Run(async () =>             
{                 
    while (!token.IsCancellationRequested) {
         Console.Write("*");                      
         await Task.Delay(1000);                
    }           
    token.ThrowIfCancellationRequested();               
}, token)
.ContinueWith(t =>
 {
      t.Exception?.Handle(e => true);
      Console.WriteLine("You have canceled the task");
 },TaskContinuationOptions.OnlyOnCanceled);  
 
Console.WriteLine("Press enter to stop the task");                 
Console.ReadLine();                 
cancellationTokenSource.Cancel();                 
task.Wait(); 

希望这能更好地理解。

2
为什么取消标记(cancelationToken)没有传递到“Task.Delay()”中? - Peter Bruins

24
你可以使用取消令牌创建任务,当你的应用程序进入后台时,你可以取消该令牌。
你可以在PCL中执行此操作 https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/app-lifecycle
var cancelToken = new CancellationTokenSource();
Task.Factory.StartNew(async () => {
    await Task.Delay(10000);
    // call web API
}, cancelToken.Token);

//this stops the Task:
cancelToken.Cancel(false);

另一个解决方案是在Xamarin.Forms中使用计时器,在应用程序进入后台时停止计时器。 https://xamarinhelp.com/xamarin-forms-timer/

4
两个问题。第一个问题,使用 Task.Run()。第二个问题,这样做不会像您想的那样取消底层的等待任务。在这种情况下,您需要将 cancelToken 传递给延迟函数:Task.Delay(10000, cancelToken)。使用令牌进行取消是协作性的,需要将它传递到您希望在链中能够取消的每个可等待对象。 - Marc L.
2
关于上述第二点,请参见此处的“CancellationToken”部分。 (https://blog.stephencleary.com/2015/03/a-tour-of-task-part-9-delegate-tasks.html)。 - Marc L.

12

您可以使用ThrowIfCancellationRequested而无需处理异常!

ThrowIfCancellationRequested的使用应该在一个Task中(而不是Thread)使用。当在Task中使用时,您无需自己处理异常(并获取未处理的异常错误)。这将导致离开Task,并且Task.IsCancelled属性将为True。不需要异常处理。

在您的特定情况下,请将Thread更改为Task

Task t = null;
try
{
    t = Task.Run(() => Work(cancelSource.Token), cancelSource.Token);
}

if (t.IsCancelled)
{
    Console.WriteLine("Canceled!");
}

你为什么使用t.Start()而不是Task.Run() - Xander Luciano
1
@XanderLuciano:在这个例子中没有特定的原因,使用Task.Run()会是更好的选择。 - Titus
3
你没有显示 ThrowIfCancellationRequested。 - Toolkit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接