处理取消令牌源的正确模式

Enr*_*one 6 c# task-parallel-library async-await cancellationtokensource .net-core

考虑这样一个场景,您需要完成一些异步工作,并且可以在即发即弃模式下运行它。这种异步工作能够侦听取消,因此您向它传递取消令牌以便能够取消它。

在给定的时刻,我们可以决定请求取消正在进行的活动,方法是使用我们从中获取取消令牌的取消令牌源对象。

因为取消令牌源实现了IDisposable,所以我们应该尽可能地调用它的Dispose方法。这个问题的重点是确定您何时完成给定的取消令牌源。

假设您决定通过调用Cancel取消令牌源上的方法来取消正在进行的工作:在调用之前是否需要等待正在进行的操作完成Dispose

换句话说,我应该这样做:

class Program 
{
  static void Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();
    cts.Dispose(); // I call Dispose immediately after cancelling without waiting for the completion of ongoing work listening to the cancellation requests via the token

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}
Run Code Online (Sandbox Code Playgroud)

或者这样:

class Program 
{
  static async Task Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    var task = DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();

    try 
    {
      await task.ConfigureAwait(false);
    }
    catch(OperationCanceledException) 
    {
      // this exception is raised by design by the cancellation
    }
    catch (Exception) 
    {
      // an error has occurred in the asynchronous work before cancellation was requested
    }

    cts.Dispose(); // I call Dispose only when I'm sure that the ongoing work has completed

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}
Run Code Online (Sandbox Code Playgroud)

其他详细信息:我所指的代码是在 ASP.NET core 2.2 Web 应用程序中编写的,这里我使用控制台应用程序场景只是为了简化我的示例。

我在 stackoverflow 上发现了类似的问题,要求处理取消令牌源对象。一些答案表明,在某些情况下,并不真正需要处理这个对象。

我对整个IDisposable主题的方法是,我总是倾向于遵守一个类的公开契约,换句话说,如果一个对象声称是一次性的,我更喜欢Dispose在我完成后总是调用它。我不喜欢根据类的实现细节来猜测是否真的需要调用 dispose 的想法,这些实现细节可能会在未来版本中以未记录的方式更改。

The*_*ias 11

CancellationTokenSource为了确保与“即发即弃”关联的CTS ( )Task最终被处置,您应该将延续附加到任务,并从延续内部处置 CTS。但这会产生一个问题,因为Cancel当对象正在处理时另一个线程可以调用该方法,并且根据文档,Dispose方法不是线程安全的:

的所有公共和受保护成员CancellationTokenSource都是线程安全的,并且可以在多个线程中同时使用,但 除外Dispose(),它只能在CancellationTokenSource对象上的所有其他操作完成后才能使用。

因此,在没有同步的情况下同时从两个不同的线程调用CancelDispose不是一种选择。这只剩下一个选项可用:在 CTS 类的所有公共成员周围添加一层同步。但这并不是一个令人愉快的选择,原因如下:

  1. 必须编写线程安全的包装类(编写代码)
  2. 每次启动可取消的即发即弃任务时都必须使用它(编写更多代码)
  3. 造成同步的性能损失
  4. 造成附加延续的性能损失
  5. 必须维护一个变得更加复杂且更容易出现错误的系统
  6. 必须处理哲学问题,为什么该类一开始就没有设计成线程安全的

因此,我的建议是采取替代方案,即仅在无法等待其关联任务完成的情况下,不处理 CTS。换句话说,如果无法将使用 CTS 的代码包含在语句中using,则只需让垃圾收集器回收保留的资源即可。这意味着您必须遵守文档的这一部分:

在释放对 的最后一个引用之前,请务必调用 Dispose CancellationTokenSourceCancellationTokenSource否则,在垃圾收集器调用该对象的方法之前,它正在使用的资源不会被释放Finalize

...和这个

该类CancellationTokenSource实现该IDisposable接口。CancellationTokenSource.Dispose当您使用完取消令牌源以释放它所持有的任何非托管资源时,您应该确保调用该方法。

如果这让您感觉有点肮脏,那么您并不孤单。Task如果您认为该类也实现了该接口,但不需要IDisposable处置任务实例,您可能会感觉更好。


Nic*_*ick 4

正确的做法是第二个CancellationTokenSource-在确定任务被取消后将 其丢弃。CancellationToken依赖来自的信息CancellationTokenSource才能正常运行。虽然当前实现的CancellationToken编写方式是,如果创建它的 CTS 被释放,即使不抛出异常,它仍然可以工作,但它可能无法正常运行或始终按预期运行。