【问题标题】:Correct pattern to dispose of cancellation token source处理取消令牌源的正确模式
【发布时间】:2020-04-22 07:24:43
【问题描述】:

考虑一个场景,您有一些异步工作要完成,您可以在“即发即弃”模式下运行它。此异步工作能够侦听取消,因此您将取消令牌传递给它以便能够取消它。

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

因为取消令牌源实现了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);
  }
}

或者这样:

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);
  }
}

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

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

我对整个 IDisposable 主题的处理方法是,我总是倾向于遵守一个类的暴露契约,换一种说法,如果一个对象声称是一次性的,我更愿意在完成后总是调用 Dispose用它。我不喜欢通过依赖类的实现细节来猜测是否真的需要调用 dispose 的想法,这些实现细节可能会在未来的版本中以未记录的方式发生变化。

【问题讨论】:

    标签: c# .net-core async-await task-parallel-library cancellationtokensource


    【解决方案1】:

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

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

    因此,从两个不同的线程同时调用CancelDispose 而不进行同步不是一种选择。这只剩下一个选项可用:围绕 CTS 类的所有公共成员添加一层同步。不过,这不是一个令人愉快的选择,原因如下:

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

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

    在释放对CancellationTokenSource 的最后引用之前,请始终调用 Dispose。否则,在垃圾收集器调用CancellationTokenSource对象的Finalize方法之前,它正在使用的资源不会被释放。

    ...和this:

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

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

    【讨论】:

    • 很好的答案!只是一个评论,我读到如果不处理,创建linkedtokensources可能会泄漏 - 你对此有什么想法吗?
    • @sommmen 是的,我也读过这个。我对此的唯一想法是:如果你必须使用链接的CancellationTokenSource 来完成即发即弃的任务,那你就有麻烦了!
    • 感谢您的贡献和链接的精彩文章
    • 附加一个延续并从那里调用 dispose 的想法很好,但我还需要能够从其他线程调用 Cancel。所以,我会选择我的线程的第二个选项。我会调用 Cancel,然后我会等待任务完成,然后我会调用 Dispose。
    • @TheodorZoulias 是的,这不是微软最好的作品。我不想在可能会公开的库中使用“CancellationToken”以外的其他东西,但在某些情况下使用它真的很痛苦。
    【解决方案2】:

    正确的做法是第二 - 在您确定任务被取消后处理CancellationTokenSourceCancellationToken 依赖来自 CancellationTokenSource 的信息才能正常运行。虽然当前实现 CancellationToken 的编写方式即使在不引发异常的情况下仍然可以在创建它的 CTS 被释放时工作,但它可能无法正常运行或始终按预期运行。

    【讨论】:

      【解决方案3】:

      像任何IDisposable 一样,您在使用完资源后将其丢弃。这是IDisposable 的硬性规定,我还没有遇到过不是这种情况的情况,但我当然愿意学习;)。

      CancellationTokenSource 的情况下,这意味着您在不再使用对象本身和Token 属性时释放源。 (我刚刚为这个声明打开了一个来源,但可惜我分心了,不知何故丢失了它)

      因此,当任务不再使用 CancellationToken 时,您将进行处置。在你的情况下,第二个选项,因为你确定没有任务使用令牌。

      编辑;除此之外,将任何实现一次性的属性设置为 null 也是一种很好的做法。在这种情况下,因为您只有局部变量,这无关紧要,但是当您将令牌源作为字段或其他内容时,请确保将该字段设置为 null,以便没有对令牌源的引用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-02-23
        • 2018-11-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-09-27
        相关资源
        最近更新 更多