为什么存在CancellationTokenRegistration并且为什么它实现了IDisposable接口?

28

我看到一些代码使用CancellationTokenRegistrationusing子句和Cancellation.Register

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

我明白你应该确保Dispose一个IDisposable,但为什么它需要实现IDisposable?它有哪些资源需要释放?它唯一具备的方法是关于相等性的。

如果不进行Dispose,会发生什么?会泄漏什么?


3
应该使用Unregister()方法来处理滥用,而不是丢弃。 - Hans Passant
8
无论是否属于“滥用”,取决于一个人是否认为 IDisposable.Dispose 的存在是为了清理资源,还是为了确保需要完成的任务得以完成(资源清理是“必要操作”中占主导地位的类型之一,但并不是唯一的类型)。 - supercat
1
@HansPassant - 它在 ASP.NET MVC 中也被广泛使用。如果框架的作者自己这样做,我认为这并不是滥用。 - Erik Funkenbusch
2个回答

29

这个模式是一种方便的方法,可以确保CancellationTokenRegistration.Unregister()被自动调用。Stephen Toub在他的.NET并行编程博客文章中经常使用它,例如这里

我明白你应该确保Dispose掉IDisposable,但为什么它要实现IDisposable呢?它有哪些资源需要释放?它唯一关心的方法是相等性。

我认为,最好的答案可以在Microsoft的Mike Liddell的.NET 4取消框架帖子中找到:

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

当回调函数注册到CancellationToken时,返回的对象是CancellationTokenRegistration。这是一个轻量结构类型,它是IDisposable,并且处理此注册对象会导致取消已注册的回调。保证在Dispose()方法返回后,已注册的回调未运行,并且不会随后启动。由此产生的后果是CancellationTokenRegistration.Dispose()必须阻止如果回调正在执行。因此,所有注册的回调应该快速,而且不会阻塞任何显着的时间。

Mike Liddell的另一份相关文档是“在.NET Framework 4中使用取消支持”(UsingCancellationinNET4.pdf)

更新:这可以在参考源代码中验证。

还要注意的是,取消回调是向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表达式捕获?而且它不需要被处理... - i3arnon
1
@l3arnon,不会的。只有在涉及潜在线程切换时,框架才会捕获ExecutionContext。如果为任何“常规”lambda回调捕获/恢复它,那将是相当大的开销。更多信息请参见此处 - noseratio - open to work
关于更新,你确定这是真的吗?注册是否与CTS(在其整个生命周期内)相关联,而不仅仅是CT? - i3arnon
1
@l3arnon,我很确定,并且我认为这是合乎逻辑的:取消请求是通过CTS而不是其任何令牌进行的。如果您需要更多详细信息,请查看此处的m_source.InternalRegister:http://referencesource.microsoft.com/#mscorlib/system/threading/CancellationToken.cs#31a5196df8e4df6d。 - noseratio - open to work

4
为什么需要实现IDisposable接口?有哪些资源需要释放?
通常,IDisposable接口用于与释放资源完全无关的事情。这是确保在一个using代码块结束时,即使发生异常,某些操作也将被完成的一种方便方式。一些人(不包括我)认为这样做是滥用了Dispose模式。
对于CancellationToken.Register方法,"某些操作"指的是回调函数的注销。
请注意,在您问题中发布的代码中,几乎肯定是错误地在CancellationTokenRegistration上使用using代码块:由于wc.DownloadStringAsync立即返回,因此在取消操作之前就立即注销回调函数,即使CancellationToken被触发,也永远不会调用wc.CancelAsync。如果await调用wc.DownloadStringAsync,则使用using代码块结束不会在wc.DownloadStringAsync完成之前到达。
如果您没有Dispose该对象,会发生什么?会泄漏什么东西?
在这种情况下,如果未注销回调函数,则会发生什么。在大多数情况下,这可能并不重要,因为它仅由取消标记引用,并且由于CancellationToken是通常仅存储在堆栈上的值类型,当其超出作用域后引用将消失。但是,如果将CancellationToken存储在堆上,则可能会发生泄漏,这取决于具体情况。

“这可能并不重要,因为它只被取消令牌引用……当它超出范围时,引用将消失。”但实际上,取消回调是与CancellationTokenSource注册的,因此当CancellationToken超出范围时,它仍然会保持活动状态。如果在同一源上仍然请求取消,则稍后可能会出现问题,更多细节。” - noseratio - open to work

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