何时处置 CancellationTokenSource?
CancellationTokenSource
类是一次性的。快速查看 Reflector 可以发现 KernelEvent 的使用,这是一种(很可能)非托管资源。 由于 CancellationTokenSource
没有终结器,如果我们不处理它,GC 就不会这样做。
另一方面,如果您查看 MSDN 文章 取消托管中列出的示例,线程,只有一个代码片段处理令牌。
在代码中处理它的正确方法是什么?
- 如果您不等待,则无法使用
using
包装启动并行任务的代码。只有当您不等待时取消才有意义。 - 当然,您可以使用
Dispose
调用在任务上添加ContinueWith
,但这是正确的方法吗? - 可取消的 PLINQ 查询怎么样,它不会同步回来,只是在最后做一些事情?假设
.ForAll(x => Console.Write(x))
? - 它可以重复使用吗?是否可以将同一个令牌用于多个调用,然后将其与主机组件(例如 UI 控制)一起处理?
因为它没有类似 Reset
方法来清理 IsCancelRequested
和 Token
字段,所以我认为它不可重用,因此每次您启动一项任务(或 PLINQ 查询)时,您应该创建一个新任务。这是真的吗?如果是,我的问题是在这么多 CancellationTokenSource
实例上处理 Dispose
的正确且推荐的策略是什么?
The class CancellationTokenSource
is disposable. A quick look in Reflector proves usage of KernelEvent
, a (very likely) unmanaged resource.
Since CancellationTokenSource
has no finalizer, if we do not dispose it, the GC won't do it.
On the other hand, if you look at the samples listed on the MSDN article Cancellation in Managed Threads, only one code snippet disposes of the token.
What is the proper way to dispose of it in code?
- You cannot wrap code starting your parallel task with
using
if you do not wait for it. And it makes sense to have cancellation only if you do not wait. - Of course you can add
ContinueWith
on task with aDispose
call, but is that the way to go? - What about cancelable PLINQ queries, which do not synchronize back, but just do something at the end? Let's say
.ForAll(x => Console.Write(x))
? - Is it reusable? Can the same token be used for several calls and then dispose it together with the host component, let's say UI control?
Because it does not have something like a Reset
method to clean-up IsCancelRequested
and Token
field I would suppose it's not reusable, thus every time you start a task (or a PLINQ query) you should create a new one. Is it true? If yes, my question is what is the correct and recommended strategy to deal with Dispose
on those many CancellationTokenSource
instances?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
说到是否真的有必要在
CancellationTokenSource
上调用 Dispose...我的项目中出现了内存泄漏,结果发现CancellationTokenSource
就是问题所在。我的项目有一项服务,它不断读取数据库并触发不同的任务,并且我将链接的取消令牌传递给我的工作人员,因此即使他们完成数据处理后,取消令牌也没有被处理,这导致了内存泄漏。
MSDN 托管线程中的取消明确指出:
我在实现中使用了ContinueWith。
Speaking about whether it's really necessary to call Dispose on
CancellationTokenSource
... I had a memory leak in my project and it turned out thatCancellationTokenSource
was the problem.My project has a service, that is constantly reading database and fires off different tasks, and I was passing linked cancellation tokens to my workers, so even after they had finished processing data, cancellation tokens weren't disposed, which caused a memory leak.
MSDN Cancellation in Managed Threads states it clearly:
I used
ContinueWith
in my implementation.我认为目前的答案都不令人满意。经过研究,我发现了 Stephen Toub 的回复(参考):
粗体部分我认为是重要的部分。他使用了“更具影响力”,这使得它有点模糊。我将其解释为在这些情况下应该调用
Dispose
,否则不需要使用Dispose
。I didn't think any of the current answers were satisfactory. After researching I found this reply from Stephen Toub (reference):
The bold part I think is the important part. He uses "more impactful" which leaves it a bit vague. I'm interpreting it as meaning calling
Dispose
in those situations should be done, otherwise usingDispose
is not needed.您应该始终处置
CancellationTokenSource
。具体如何处置取决于具体场景。您提出了几种不同的方案。
using
仅当您在等待的某些并行工作上使用CancellationTokenSource
时才有效。如果这就是您的情况,那就太好了,这是最简单的方法。使用任务时,请按照您指示的那样使用
ContinueWith
任务来处置CancellationTokenSource
。对于 plinq,您可以使用
using
,因为您并行运行它,但等待所有并行运行的工作线程完成。对于 UI,您可以为每个不绑定到单个取消触发器的可取消操作创建一个新的
CancellationTokenSource
。维护一个List
并将每个源添加到列表中,在处置组件时处置所有源。对于线程,创建一个新线程来连接所有工作线程,并在所有工作线程完成时关闭单个源。请参阅 CancellationTokenSource,何时处置?
总有办法的。
IDisposable
实例应始终被释放。示例通常不会这样做,因为它们要么是显示核心用法的快速示例,要么是因为添加所演示的类的所有方面对于示例来说过于复杂。该示例只是一个示例,不一定(甚至通常)生产质量代码。并非所有示例都可以按原样复制到生产代码中。You should always dispose
CancellationTokenSource
.How to dispose it depends exactly on the scenario. You propose several different scenarios.
using
only works when you're usingCancellationTokenSource
on some parallel work that you're waiting. If that's your senario, then great, it's the easiest method.When using tasks, use a
ContinueWith
task as you indicated to dispose ofCancellationTokenSource
.For plinq you can use
using
since you're running it in parallel but waiting on all of the parallel running workers to finish.For UI, you can create a new
CancellationTokenSource
for each cancellable operation that is not tied to a single cancel trigger. Maintain aList<IDisposable>
and add each source to the list, disposing all of them when your component is disposed.For threads, create a new thread that joins all the worker threads and closes the single source when all of the worker threads finished. See CancellationTokenSource, When to dispose?
There's always a way.
IDisposable
instances should always be disposed. Samples often don't because they're either quick samples to show core usage or because adding in all aspects of the class being demonstrated would be overly complex for a sample. The sample is just that a sample, not necessarily (or even usually) production quality code. Not all samples are acceptable to be copied into production code as is.自从我问这个问题并得到许多有用的答案以来已经很长时间了,但我遇到了一个与此相关的有趣问题,并认为我会将其发布在这里作为另一个答案:
您应该调用
CancellationTokenSource.Dispose()< /code> 仅当您确定没有人会尝试获取 CTS 的
Token
属性时。否则,您不应该调用Dispose()
,因为它会产生竞争条件。例如,请参见此处:https://github.com/aspnet/AspNetKatana/issues/108
在此问题的修复中,之前执行 cts.Cancel() 的代码; cts.Dispose(); 已被编辑为仅执行
cts.Cancel();
因为有人不幸尝试获取取消令牌以观察其取消状态不幸的是,在调用Dispose
后,除了他们计划的OperationCanceledException
之外,还需要处理ObjectDisposeException
。Tratcher 提出了与此修复相关的另一个关键观察结果:“只有不会被取消的代币才需要进行处置,因为取消会进行所有相同的清理。”
即只需执行
Cancel()
而不是处理就足够好了!It has been a long time since I asked this and got many helpful answers but I came across an interesting issue related to this and thought I would post it here as another answer of sorts:
You should call
CancellationTokenSource.Dispose()
only when you are sure that nobody is going to try to get the CTS'sToken
property. Otherwise you should not callDispose()
, because it creates a race condition. For instance, see here:https://github.com/aspnet/AspNetKatana/issues/108
In the fix for this issue, code which previously did
cts.Cancel(); cts.Dispose();
was edited to just docts.Cancel();
because anyone so unlucky as to try to get the cancellation token in order to observe its cancellation state afterDispose
has been called will unfortunately also need to handleObjectDisposedException
- in addition to theOperationCanceledException
that they were planning for.Another key observation related to this fix is made by Tratcher: "Disposal is only required for tokens that won't be cancelled, as cancellation does all of the same cleanup."
i.e. just doing
Cancel()
instead of disposing is really good enough!我在 ILSpy 中查看了
CancellationTokenSource
但我只能找到m_KernelEvent
它实际上是一个ManualResetEvent
,它是ManualResetEvent
的包装类代码>WaitHandle 对象。这应该由 GC 正确处理。I took a look in ILSpy for the
CancellationTokenSource
but I can only findm_KernelEvent
which is actually aManualResetEvent
, which is a wrapper class for aWaitHandle
object. This should be handled properly by the GC.这个答案仍然出现在谷歌搜索中,我相信投票的答案并没有给出完整的故事。查看
CancellationTokenSource
源代码后> (CTS) 和CancellationToken
(CT) 我相信对于大多数用例,以下代码序列就可以了:上面提到的
m_kernelHandle
内部字段是同步对象支持CTS 和 CT 类中的WaitHandle
属性。仅当您访问该属性时才会实例化它。因此,除非您在Task
中使用WaitHandle
进行一些老式线程同步,否则调用 dispose 不会产生任何效果。当然,如果您正在使用它,您应该按照上面其他答案的建议进行操作,并延迟调用
Dispose
,直到使用WaitHandle
操作句柄已完成,因为如 WaitHandle 的 Windows API 文档,结果未定义。This answer is still coming up in Google searches, and I believe the voted up answer does not give the full story. After looking over the source code for
CancellationTokenSource
(CTS) andCancellationToken
(CT) I believe that for most use cases the following code sequence is fine:The
m_kernelHandle
internal field mentioned above is the synchronization object backing theWaitHandle
property in both the CTS and CT classes. It is only instantiated if you access that property. So, unless you are usingWaitHandle
for some old-school thread synchronization in yourTask
calling dispose will have no effect.Of course, if you are using it you should do what is suggested by the other answers above and delay calling
Dispose
until anyWaitHandle
operations using the handle are complete, because, as is described in the Windows API documentation for WaitHandle, the results are undefined.我编写了一个线程安全类,它绑定
CancellationTokenSource< /code>
到一个
Task
,并保证CancellationTokenSource
将在其关联的Task
完成时被释放。它使用锁来确保CancellationTokenSource
在释放期间或之后不会被取消。发生这种情况是为了遵守文档 ,其中指出:并且还有:
下面是
CancelableExecution
类:CancelableExecution
类的主要方法是RunAsync
和Cancel
。默认情况下,不允许并发(重叠)操作,这意味着在开始新操作之前,第二次调用 RunAsync 将静默取消并等待上一个操作完成(如果它仍在运行)。此类可用于任何类型的应用程序。它的主要用途是在 UI 应用程序中,在带有用于启动和取消异步操作的按钮的表单内,或者在每次更改所选项目时取消和重新启动操作的列表框。以下是第一个用例的示例:
RunAsync
方法接受额外的CancellationToken
作为参数,该参数链接到内部创建的CancellationTokenSource
。提供此可选令牌在高级场景中可能很有用。对于与 .NET Framework 兼容的版本,您可以查看此答案的第三版。
I wrote a thread-safe class that binds a
CancellationTokenSource
to aTask
, and guarantees that theCancellationTokenSource
will be disposed when its associatedTask
completes. It uses locks to ensure that theCancellationTokenSource
will not be canceled during or after it has been disposed. This happens for compliance with the documentation, that states:And also:
Here is the
CancelableExecution
class:The primary methods of the
CancelableExecution
class are theRunAsync
and theCancel
. By default concurrent (overlapping) operations are not allowed, meaning that callingRunAsync
a second time will silently cancel and await the completion of the previous operation (if it's still running), before starting the new operation.This class can be used in applications of any kind. Its primary intended usage though is in UI applications, inside forms with buttons for starting and canceling an asynchronous operation, or with a listbox that cancels and restarts an operation every time its selected item is changed. Here is an example of the first use-case:
The
RunAsync
method accepts an extraCancellationToken
as argument, that is linked to the internally createdCancellationTokenSource
. Supplying this optional token may be useful in advanced scenarios.For a version compatible with the .NET Framework, you can look at the 3rd revision of this answer.