【问题标题】:Why CancellationTokenRegistration exists and why does it implement IDisposable为什么存在 CancellationTokenRegistration 以及为什么它实现 IDisposable
【发布时间】:2014-01-26 18:49:54
【问题描述】:

我已经看到在CancellationTokenRegistration 结果上使用Cancellation.Registerusing 子句的代码:

using (CancellationTokenRegistration ctr = token.Register(() => wc.CancelAsync()))
{
    await wc.DownloadStringAsync(new Uri("http://www.hamster.com"));
}

我知道你应该确保你是 DisposeIDisposable,但为什么它甚至实现了 IDisposable?它必须释放哪些资源?它唯一的方法是关于平等的。

如果你不Dispose 会发生什么?你泄露了什么?

【问题讨论】:

  • 这是 dispose 滥用,它应该有一个 Unregister() 方法。
  • @HansPassant:是否“滥用”取决于一个人是否认为IDisposable.Dispose 存在是为了清理资源,或者是为了确保需要完成的事情,完成(资源清理是“必要行动”的主要类型,但不是唯一的)。
  • @HansPassant - 它也在 asp.net MVC 中大量使用。如果框架作者自己做,我不认为这是滥用。

标签: c# .net task-parallel-library idisposable cancellation-token


【解决方案1】:

此模式是确保自动调用CancellationTokenRegistration.Unregister() 的便捷方式。 Stephen Toub 在他的 Parallel Programming with .NET 博客文章中经常使用它,例如here.

我知道你应该确保你 Dispose 一个 IDisposable,但是为什么 它甚至实现了 IDisposable 吗?它需要什么资源 释放?它唯一的方法是关于平等的。

IMO,对此问题的最佳答案可以在 Microsoft 的 Mike Liddell 的 .NET 4 Cancellation Framework 帖子中找到:

当回调注册到 CancellationToken 时,当前 捕获线程的ExecutionContext,以便运行回调 具有完全相同的安全上下文。的捕获 当前线程的同步上下文是可选的,可以请求 如果需要,通过ct.Register() 的重载。回调通常是 存储,然后在请求取消时运行,但如果回调 在请求取消后注册,回调将 立即在当前线程上运行,或通过Send() 在当前线程上运行 SynchronizationContext 如果适用。

当回调注册到 CancellationToken 时,返回的 对象是CancellationTokenRegistration。这是一个轻型结构类型 即IDiposable,并且处理此注册对象会导致 要注销的回调。保证之后 Dispose()方法已经返回,注册的回调既不是 运行也不会随后开始。这样做的结果是 CancellationTokenRegistration.Dispose() 必须阻塞,如果回调是 目前正在执行。因此,所有注册的回调都应该很快 并且不会阻塞很长时间。

Mike Liddell 的另一个相关文档是"Using Cancellation Support in .NET Framework 4" (UsingCancellationinNET4.pdf)

更新,这是可验证的here in the Reference Source

还需要注意的是,取消回调是使用CancellationTokenSource 注册的,而不是CancellationToken。因此,如果CancellationTokenRegistration.Dispose() 的范围不正确,则注册将在父CancellationTokenSource 对象的生命周期内保持活动状态。当异步操作的范围结束时,这可能会导致意外回调,例如:

async Task TestAsync(WebClient wc, CancellationToken token)
{
    token.Register(() => wc.CancelAsync());
    await wc.DownloadStringAsync(new Uri("http://www.hamster.com"));
}

// CancellationTokenSource.Cancel() may still get called later,
// in which case wc.CancelAsync() will be invoked too

因此,使用using 来确定一次性CancellationTokenRegistration 的范围很重要(或使用try/finally 显式调用CancellationTokenRegistration.Dispose())。

【讨论】:

  • 难道 ExecutionContext 也不会在“常规”lambda 表达式中被捕获吗?并且不需要处理...
  • @l3arnon,不,不会。 ExecutionContext 仅在涉及潜在线程切换时由框架捕获。如果为任何“常规” lambda 回调捕获/恢复它,这将是一个相当大的开销。更多 here.
  • 关于更新,你确定这是真的吗?注册是否与 CTS 相关联(贯穿其整个生命周期),而不仅仅是 CT?
  • @l3arnon,我敢肯定,而且我认为这是合乎逻辑的:通过 CTS 请求取消,而不是通过其令牌之一。如果您需要更多详细信息,请查看this 以获得m_source.InternalRegister
【解决方案2】:

为什么它甚至实现了 IDisposable?它要释放什么资源?

IDisposable 常用于与释放资源完全无关的事情;这是一种确保在using 块结束时完成某些操作的便捷方法,即使发生异常也是如此。有些人(不是我)认为这样做是对Dispose 模式的滥用。

对于CancellationToken.Register,“某事”是回调的注销。

请注意,在您在问题中发布的代码中,在CancellationTokenRegistration 上使用using 块几乎肯定是一个错误:由于wc.DownloadStringAsync 立即返回,回调将立即取消注册,在操作有机会被取消之前,因此永远不会调用wc.CancelAsync,即使CancellationToken 已发出信号。如果等待对wc.DownloadStringAsync 的调用是有意义的,因为在这种情况下,在wc.DownloadStringAsync 完成之前不会到达using 块的末尾。 (编辑:不是自从问题被编辑后不再正确)

如果您不丢弃它会怎样?你泄露了什么?

在这种情况下,回调并未取消注册。 这可能并不重要,因为它只被取消标记引用,并且由于CancellationToken 是一个通常只存储在堆栈中的值类型,当它超出范围时引用将消失。所以在大多数情况下它不会泄漏任何东西,但如果你将CancellationToken 存储在堆的某个地方,它可能会泄漏。 编辑:实际上,这是不正确的;请参阅 Noseratio 的答案以获得解释

【讨论】:

  • “这可能并不重要,因为它只被取消令牌引用......当它超出范围时引用将消失” - 实际上,取消回调在CancellationTokenSource 中注册,因此当CancellationToken 超出范围时它将保持活动状态。如果在同一来源more details 上仍然请求取消,这可能会在以后出现问题。
猜你喜欢
  • 2012-07-04
  • 2012-01-19
  • 2016-02-13
  • 2020-03-23
  • 1970-01-01
  • 2014-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多