何时处置 CancellationTokenSource?

发布于 2024-11-28 09:57:55 字数 895 浏览 0 评论 0原文

CancellationTokenSource 类是一次性的。快速查看 Reflector 可以发现 KernelEvent 的使用,这是一种(很可能)非托管资源。 由于 CancellationTokenSource 没有终结器,如果我们不处理它,GC 就不会这样做。

另一方面,如果您查看 MSDN 文章 取消托管中列出的示例,线程,只有一个代码片段处理令牌。

在代码中处理它的正确方法是什么?

  1. 如果您不等待,则无法使用 using 包装启动并行任务的代码。只有当您不等待时取消才有意义。
  2. 当然,您可以使用 Dispose 调用在任务上添加 ContinueWith,但这是正确的方法吗?
  3. 可取消的 PLINQ 查询怎么样,它不会同步回来,只是在最后做一些事情?假设.ForAll(x => Console.Write(x))
  4. 它可以重复使用吗?是否可以将同一个令牌用于多个调用,然后将其与主机组件(例如 UI 控制)一起处理?

因为它没有类似 Reset 方法来清理 IsCancelRequestedToken 字段,所以我认为它不可重用,因此每次您启动一项任务(或 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?

  1. 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.
  2. Of course you can add ContinueWith on task with a Dispose call, but is that the way to go?
  3. 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))?
  4. 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

青瓷清茶倾城歌 2024-12-05 09:57:55

说到是否真的有必要在 CancellationTokenSource 上调用 Dispose...我的项目中出现了内存泄漏,结果发现 CancellationTokenSource 就是问题所在。

我的项目有一项服务,它不断读取数据库并触发不同的任务,并且我将链接的取消令牌传递给我的工作人员,因此即使他们完成数据处理后,取消令牌也没有被处理,这导致了内存泄漏。

MSDN 托管线程中的取消明确指出:

请注意,使用完链接的令牌源后,您必须对它调用Dispose。有关更完整的示例,请参阅 如何:监听多个取消请求

我在实现中使用了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 that CancellationTokenSource 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:

Notice that you must call Dispose on the linked token source when you are done with it. For a more complete example, see How to: Listen for Multiple Cancellation Requests.

I used ContinueWith in my implementation.

不打扰别人 2024-12-05 09:57:55

我认为目前的答案都不令人满意。经过研究,我发现了 Stephen Toub 的回复(参考):

这要看情况。
在 .NET 4 中,CTS.Dispose 有两个主要用途。如果
CancellationToken 的 WaitHandle 已被访问(因此延迟
分配它),Dispose 将处理该句柄。另外,如果
CTS 是通过 CreateLinkedTokenSource 方法 Dispose 创建的
将取消 CTS 与它所链接的代币的链接。在.NET 4.5中,
Dispose 还有一个额外的目的,就是如果 CTS 使用 Timer
在幕后(例如 CancelAfter 被调用),计时器将
已处置。

CancellationToken.WaitHandle 很少被使用,
因此,事后清理通常不是使用 Dispose 的充分理由。
但是,如果您使用 CreateLinkedTokenSource 创建 CTS,或者
如果您使用 CTS 的计时器功能,它的影响力会更大
使用处置。

粗体部分我认为是重要的部分。他使用了“更具影响力”,这使得它有点模糊。我将其解释为在这些情况下应该调用 Dispose ,否则不需要使用 Dispose

I didn't think any of the current answers were satisfactory. After researching I found this reply from Stephen Toub (reference):

It depends.
In .NET 4, CTS.Dispose served two primary purposes. If the
CancellationToken's WaitHandle had been accessed (thus lazily
allocating it), Dispose will dispose of that handle. Additionally, if
the CTS was created via the CreateLinkedTokenSource method, Dispose
will unlink the CTS from the tokens it was linked to. In .NET 4.5,
Dispose has an additional purpose, which is if the CTS uses a Timer
under the covers (e.g. CancelAfter was called), the Timer will be
Disposed.

It's very rare for CancellationToken.WaitHandle to be used,
so cleaning up after it typically isn't a great reason to use Dispose.
If, however, you're creating your CTS with CreateLinkedTokenSource, or
if you're using the CTS' timer functionality, it can be more impactful
to use Dispose.

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 using Dispose is not needed.

万水千山粽是情ミ 2024-12-05 09:57:55

您应该始终处置 CancellationTokenSource

具体如何处置取决于具体场景。您提出了几种不同的方案。

  1. using 仅当您在等待的某些并行工作上使用 CancellationTokenSource 时才有效。如果这就是您的情况,那就太好了,这是最简单的方法。

  2. 使用任务时,请按照您指示的那样使用 ContinueWith 任务来处置 CancellationTokenSource

  3. 对于 plinq,您可以使用 using,因为您并行运行它,但等待所有并行运行的工作线程完成。

  4. 对于 UI,您可以为每个不绑定到单个取消触发器的可取消操作创建一个新的 CancellationTokenSource。维护一个 List 并将每个源添加到列表中,在处置组件时处置所有源。

  5. 对于线程,创建一个新线程来连接所有工作线程,并在所有工作线程完成时关闭单个源。请参阅 CancellationTokenSource,何时处置?

总有办法的。 IDisposable 实例应始终被释放。示例通常不会这样做,因为它们要么是显示核心用法的快速示例,要么是因为添加所演示的类的所有方面对于示例来说过于复杂。该示例只是一个示例,不一定(甚至通常)生产质量代码。并非所有示例都可以按原样复制到生产代码中。

You should always dispose CancellationTokenSource.

How to dispose it depends exactly on the scenario. You propose several different scenarios.

  1. using only works when you're using CancellationTokenSource on some parallel work that you're waiting. If that's your senario, then great, it's the easiest method.

  2. When using tasks, use a ContinueWith task as you indicated to dispose of CancellationTokenSource.

  3. For plinq you can use using since you're running it in parallel but waiting on all of the parallel running workers to finish.

  4. For UI, you can create a new CancellationTokenSource for each cancellable operation that is not tied to a single cancel trigger. Maintain a List<IDisposable> and add each source to the list, disposing all of them when your component is disposed.

  5. 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.

你的背包 2024-12-05 09:57:55

自从我问这个问题并得到许多有用的答案以来已经很长时间了,但我遇到了一个与此相关的有趣问题,并认为我会将其发布在这里作为另一个答案:

您应该调用 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's Token property. Otherwise you should not call Dispose(), 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 do cts.Cancel(); because anyone so unlucky as to try to get the cancellation token in order to observe its cancellation state after Dispose has been called will unfortunately also need to handle ObjectDisposedException - in addition to the OperationCanceledException 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!

不如归去 2024-12-05 09:57:55

我在 ILSpy 中查看了 CancellationTokenSource 但我只能找到 m_KernelEvent 它实际上是一个 ManualResetEvent,它是 ManualResetEvent 的包装类代码>WaitHandle 对象。这应该由 GC 正确处理。

I took a look in ILSpy for the CancellationTokenSource but I can only find m_KernelEvent which is actually a ManualResetEvent, which is a wrapper class for a WaitHandle object. This should be handled properly by the GC.

一刻暧昧 2024-12-05 09:57:55

这个答案仍然出现在谷歌搜索中,我相信投票的答案并没有给出完整的故事。查看 CancellationTokenSource源代码后> (CTS) 和 CancellationToken (CT) 我相信对于大多数用例,以下代码序列就可以了:

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

上面提到的 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) and CancellationToken (CT) I believe that for most use cases the following code sequence is fine:

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}

The m_kernelHandle internal field mentioned above is the synchronization object backing the WaitHandle property in both the CTS and CT classes. It is only instantiated if you access that property. So, unless you are using WaitHandle for some old-school thread synchronization in your Task 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 any WaitHandle operations using the handle are complete, because, as is described in the Windows API documentation for WaitHandle, the results are undefined.

残疾 2024-12-05 09:57:55

我编写了一个线程安全类,它绑定 CancellationTokenSource< /code>到一个 Task,并保证 CancellationTokenSource 将在其关联的 Task 完成时被释放。它使用锁来确保 CancellationTokenSource 在释放期间或之后不会被取消。发生这种情况是为了遵守文档 ,其中指出:

只有当 CancellationTokenSource 对象上的所有其他操作都已完成时,才必须使用 Dispose 方法。

并且还有

Dispose 方法使 CancellationTokenSource 处于不可用状态。

下面是 CancelableExecution 类:

public class CancelableExecution
{
    private readonly bool _allowConcurrency;
    private Operation _activeOperation;

    // Represents a cancelable operation that signals its completion when disposed
    private class Operation : IDisposable
    {
        private readonly CancellationTokenSource _cts;
        private readonly TaskCompletionSource _completionSource;
        private bool _disposed;

        public Task Completion => _completionSource.Task; // Never fails

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource(
                TaskCreationOptions.RunContinuationsAsynchronously);
        }

        public void Cancel() { lock (this) if (!_disposed) _cts.Cancel(); }

        void IDisposable.Dispose() // It is disposed once and only once
        {
            try { lock (this) { _cts.Dispose(); _disposed = true; } }
            finally { _completionSource.SetResult(); }
        }
    }

    public CancelableExecution(bool allowConcurrency)
    {
        _allowConcurrency = allowConcurrency;
    }
    public CancelableExecution() : this(false) { }

    public bool IsRunning => Volatile.Read(ref _activeOperation) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> action,
        CancellationToken extraToken = default)
    {
        ArgumentNullException.ThrowIfNull(action);
        CancellationTokenSource cts = CancellationTokenSource
            .CreateLinkedTokenSource(extraToken);
        using Operation operation = new(cts);
        // Set this as the active operation
        Operation oldOperation = Interlocked
            .Exchange(ref _activeOperation, operation);
        try
        {
            if (oldOperation is not null && !_allowConcurrency)
            {
                oldOperation.Cancel();
                // The Operation.Completion never fails.
                await oldOperation.Completion; // Continue on captured context.
            }
            cts.Token.ThrowIfCancellationRequested();
            // Invoke the action on the initial SynchronizationContext.
            Task<TResult> task = action(cts.Token);
            return await task.ConfigureAwait(false);
        }
        finally
        {
            // If this is still the active operation, set it back to null.
            Interlocked.CompareExchange(ref _activeOperation, null, operation);
        }
        // The operation is disposed here, along with the cts.
    }

    public Task RunAsync(Func<CancellationToken, Task> action,
        CancellationToken extraToken = default)
    {
        ArgumentNullException.ThrowIfNull(action);
        return RunAsync<object>(async ct =>
        {
            await action(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        Operation operation = Volatile.Read(ref _activeOperation);
        if (operation is null) return Task.CompletedTask;
        operation.Cancel();
        return operation.Completion;
    }

    public bool Cancel() => CancelAsync().IsCompleted == false;
}

CancelableExecution 类的主要方法是 RunAsyncCancel。默认情况下,不允许并发(重叠)操作,这意味着在开始新操作之前,第二次调用 RunAsync 将静默取消并等待上一个操作完成(如果它仍在运行)。

此类可用于任何类型的应用程序。它的主要用途是在 UI 应用程序中,在带有用于启动和取消异步操作的按钮的表单内,或者在每次更改所选项目时取消和重新启动操作的列表框。以下是第一个用例的示例:

private readonly CancelableExecution _cancelableExecution = new();

private async void btnExecute_Click(object sender, EventArgs e)
{
    string result;
    try
    {
        Cursor = Cursors.WaitCursor;
        btnExecute.Enabled = false;
        btnCancel.Enabled = true;
        result = await _cancelableExecution.RunAsync(async ct =>
        {
            await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
            return "Hello!";
        });
    }
    catch (OperationCanceledException)
    {
        return;
    }
    finally
    {
        btnExecute.Enabled = true;
        btnCancel.Enabled = false;
        Cursor = Cursors.Default;
    }
    this.Text += result;
}

private void btnCancel_Click(object sender, EventArgs e)
{
    _cancelableExecution.Cancel();
}

RunAsync 方法接受额外的 CancellationToken 作为参数,该参数链接到内部创建的 CancellationTokenSource 。提供此可选令牌在高级场景中可能很有用。

对于与 .NET Framework 兼容的版本,您可以查看此答案的第三版

I wrote a thread-safe class that binds a CancellationTokenSource to a Task, and guarantees that the CancellationTokenSource will be disposed when its associated Task completes. It uses locks to ensure that the CancellationTokenSource will not be canceled during or after it has been disposed. This happens for compliance with the documentation, that states:

The Dispose method must only be used when all other operations on the CancellationTokenSource object have completed.

And also:

The Dispose method leaves the CancellationTokenSource in an unusable state.

Here is the CancelableExecution class:

public class CancelableExecution
{
    private readonly bool _allowConcurrency;
    private Operation _activeOperation;

    // Represents a cancelable operation that signals its completion when disposed
    private class Operation : IDisposable
    {
        private readonly CancellationTokenSource _cts;
        private readonly TaskCompletionSource _completionSource;
        private bool _disposed;

        public Task Completion => _completionSource.Task; // Never fails

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource(
                TaskCreationOptions.RunContinuationsAsynchronously);
        }

        public void Cancel() { lock (this) if (!_disposed) _cts.Cancel(); }

        void IDisposable.Dispose() // It is disposed once and only once
        {
            try { lock (this) { _cts.Dispose(); _disposed = true; } }
            finally { _completionSource.SetResult(); }
        }
    }

    public CancelableExecution(bool allowConcurrency)
    {
        _allowConcurrency = allowConcurrency;
    }
    public CancelableExecution() : this(false) { }

    public bool IsRunning => Volatile.Read(ref _activeOperation) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> action,
        CancellationToken extraToken = default)
    {
        ArgumentNullException.ThrowIfNull(action);
        CancellationTokenSource cts = CancellationTokenSource
            .CreateLinkedTokenSource(extraToken);
        using Operation operation = new(cts);
        // Set this as the active operation
        Operation oldOperation = Interlocked
            .Exchange(ref _activeOperation, operation);
        try
        {
            if (oldOperation is not null && !_allowConcurrency)
            {
                oldOperation.Cancel();
                // The Operation.Completion never fails.
                await oldOperation.Completion; // Continue on captured context.
            }
            cts.Token.ThrowIfCancellationRequested();
            // Invoke the action on the initial SynchronizationContext.
            Task<TResult> task = action(cts.Token);
            return await task.ConfigureAwait(false);
        }
        finally
        {
            // If this is still the active operation, set it back to null.
            Interlocked.CompareExchange(ref _activeOperation, null, operation);
        }
        // The operation is disposed here, along with the cts.
    }

    public Task RunAsync(Func<CancellationToken, Task> action,
        CancellationToken extraToken = default)
    {
        ArgumentNullException.ThrowIfNull(action);
        return RunAsync<object>(async ct =>
        {
            await action(ct).ConfigureAwait(false);
            return null;
        }, extraToken);
    }

    public Task CancelAsync()
    {
        Operation operation = Volatile.Read(ref _activeOperation);
        if (operation is null) return Task.CompletedTask;
        operation.Cancel();
        return operation.Completion;
    }

    public bool Cancel() => CancelAsync().IsCompleted == false;
}

The primary methods of the CancelableExecution class are the RunAsync and the Cancel. By default concurrent (overlapping) operations are not allowed, meaning that calling RunAsync 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:

private readonly CancelableExecution _cancelableExecution = new();

private async void btnExecute_Click(object sender, EventArgs e)
{
    string result;
    try
    {
        Cursor = Cursors.WaitCursor;
        btnExecute.Enabled = false;
        btnCancel.Enabled = true;
        result = await _cancelableExecution.RunAsync(async ct =>
        {
            await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
            return "Hello!";
        });
    }
    catch (OperationCanceledException)
    {
        return;
    }
    finally
    {
        btnExecute.Enabled = true;
        btnCancel.Enabled = false;
        Cursor = Cursors.Default;
    }
    this.Text += result;
}

private void btnCancel_Click(object sender, EventArgs e)
{
    _cancelableExecution.Cancel();
}

The RunAsync method accepts an extra CancellationToken as argument, that is linked to the internally created CancellationTokenSource. 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文