何时处置CancellationTokenSource?

Geo*_*dze 142 c# parallel-extensions plinq task-parallel-library c#-4.0

这个班级CancellationTokenSource是一次性的.快速浏览Reflector证明KernelEvent了(很可能)非托管资源的使用.由于CancellationTokenSource没有终结器,如果我们不处理它,GC将不会这样做.

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

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

  1. using如果您不等待它,则无法将启动并行任务的代码包装起来.只有在你不等的时候取消才有意义.
  2. 当然你可以ContinueWith通过Dispose电话添加任务,但这是要走的路吗?
  3. 那些可以取消同步的可取消的PLINQ查询呢,但最后只做一些事情?我们说吧.ForAll(x => Console.Write(x))
  4. 它可以重复使用吗?是否可以将相同的令牌用于多个调用,然后将其与主机组件一起处理,让我们说UI控件?

因为它没有类似于Reset清理IsCancelRequestedToken字段的方法,所以我认为它不可重复使用,因此每次启动任务(或PLINQ查询)时都应该创建一个新任务.这是真的吗?如果是,我的问题是Dispose在这些CancellationTokenSource案例中处理的正确和建议的策略是什么?

Gru*_*kin 67

谈到是否真的有必要调用Dispose on CancellationTokenSource...我的项目中有内存泄漏,结果证明这CancellationTokenSource是问题所在.

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

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

请注意,完成后必须调用Dispose链接的令牌源.有关更完整的示例,请参见如何:侦听多个取消请求.

我用ContinueWith在我的实现中.

  • 这是Bryan Crosby当前接受的答案中的一个重要遗漏 - 如果你创建一个_linked_ CTS,你就会冒内存泄漏的风险.该场景非常类似于从未注册的事件处理程序. (13认同)
  • 由于同样的问题,我有一个泄漏.使用分析器我可以看到包含对链接的CTS实例的引用的回调注册.检查CTS Dispose实现的代码[here](http://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs,548)非常有见地,并强调@SørenBoisen与事件处理程序注册泄漏的比较. (4认同)
  • 2020 年的文档明确指出:“重要:CancellationTokenSource 类实现了 IDisposable 接口。当您使用完取消令牌源以释放它所持有的任何非托管资源时,您应该确保调用 CancellationTokenSource.Dispose 方法。` - https://learn.microsoft.com/en-us/dotnet/standard/threading/在托管线程中取消?view=netframework-4.8 (2认同)

Jes*_*ood 36

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

这取决于.在.NET 4中,CTS.Dispose有两个主要用途.如果已经访问了CancellationToken的WaitHandle(因此懒得分配它),Dispose将处理该句柄.此外,如果CTS是通过CreateLinkedTokenSource方法创建的,则Dispose将取消CTS与其链接的标记的链接.在.NET 4.5中,Dispose有一个额外的用途,即如果CTS使用了一个定时器(例如,调用CancelAfter),则Timer将被Disposed.

使用CancellationToken.WaitHandle是非常罕见的,因此在使用Dispose之后进行清理通常不是一个很好的理由. 但是,如果您使用CreateLinkedTokenSource创建CTS,或者如果您正在使用CTS的计时器功能,则使用Dispose可能会更有影响力.

我认为大胆的部分是重要的部分.他使用"更有影响力",这使得它有点模糊.我将其解释为意味着Dispose应该在这些情况下进行调用,否则Dispose不需要使用.

  • 更有影响力意味着将子CTS添加到父级CTS.如果您不处理孩子,如果父母长寿,将会有泄漏.因此处理链接的关键是至关重要的. (9认同)
  • Stephen Toub 的另一条评论是[我刚刚发现](https://github.com/dotnet/runtime/issues/29970#issuecomment-717840778“CancellationTokenSource 很难正确使用”):*“处理掉一次性的东西很好但有时,特别是在处理异步代码时,或者在处理所有权或生命周期不明确的代码时,需要权衡这样一个一般准则的好处/风险。对于 CTS,最重要的是使用 CreateLinkedTokenSource 创建时将其丢弃,并在超时使用时再次丢弃。"* 日期为 2020 年 10 月 28 日 (2认同)

Bry*_*sby 25

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

  • 我有同样的感觉,GC将清理所有.我会尽力验证.为什么Microsoft在这种情况下实施了处置?要摆脱事件回调并避免传播到第二代GC.在这种情况下,调用Dispose是可选的 - 如果可以,请调用它,如果不是忽略它.不是我想的最好的方式. (6认同)
  • 我的上述评论不适用于链接令牌来源; 我无法证明将这些不妥协留下来是可以的,而且这个帖子和MSDN的智慧暗示它可能不是. (5认同)
  • 我已经调查了这个问题.CancellationTokenSource获取垃圾.您可以帮助处理在GEN 1 GC中进行处理.公认. (4认同)
  • 我独立进行了同样的调查并得出了相同的结论:如果可以轻松处置,但不要担心在罕见但并非闻所未闻的情况下尝试这样做,在这种情况下,您已将 CancellationToken 发送到乡下,不想等他们回写明信片告诉你他们已经完成了。由于使用 CancellationToken 的性质,这种情况会时不时发生,我保证这真的没问题。 (2认同)

Sam*_*eff 21

你应该随时处置CancellationTokenSource.

如何处理它完全取决于场景.您提出了几种不同的方案.

  1. using只有当你正在使用CancellationTokenSource你正在等待的某些并行工作时才有效.如果这是你的senario,那么很棒,这是最简单的方法.

  2. 使用任务时,请使用ContinueWith您指示处置的任务CancellationTokenSource.

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

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

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

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

  • 有警告.如果在"等待"操作时取消CTS,则可能由于"OperationCanceledException"而恢复.然后你可以调用`Dispose()`.但是如果有操作仍然在运行并使用相应的`CancellationToken`,那么即使源处理掉,该令牌仍然会将`CanBeCanceled`报告为'true`.如果他们试图注册取消回调,**BOOM!**,`ObjectDisposedException`.在成功完成操作后调用`Dispose()`足够安全.当你真的需要取消某些东西时,它真的很棘手*. (12认同)
  • 由于Mike Strobel给出的原因而被低估 - 强制规则总是调用Dispose可以让你在处理CTS和Task时因为它们的异步性而陷入毛茸茸的境地.该规则应该是:始终处置_linked_令牌源. (7认同)

jly*_*ith 15

这个答案仍在谷歌搜索中出现,我相信投票的答案并不是完整的故事.查看(CTS)和(CT)的源代码后,我相信对于大多数用例,下面的代码序列很好:CancellationTokenSourceCancellationToken

if (cancelTokenSource != null)
{
    cancelTokenSource.Cancel();
    cancelTokenSource.Dispose();
    cancelTokenSource = null;
}
Run Code Online (Sandbox Code Playgroud)

m_kernelHandle上面提到的内部字段是支持WaitHandleCTS和CT类中的属性的同步对象.只有在您访问该属性时才会实例化它.因此,除非您WaitHandleTask调用dispose 中使用某些旧式线程同步,否则将无效.

当然,如果您正在使用它,您应该执行上面其他答案所建议的并延迟调用,Dispose直到WaitHandle使用句柄的任何操作完成,因为,如WaitHandleWindows API文档中所述,结果是未定义的.

  • MSDN文章[托管线程中的取消](https://msdn.microsoft.com/en-us/library/dd997364.aspx)声明:"监听器通过轮询,回调来监视令牌的`IsCancellationRequested`属性的值,或等待处理." 换句话说:它可能不是_you_(即发出异步请求的那个)使用等待句柄的人,它可能是监听器(即接听请求的人).这意味着,作为负责处理的人,您实际上无法控制是否使用等待句柄. (6认同)

Tim*_*ith 6

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

CancellationTokenSource.Dispose()仅当您确定没有人会尝试获得CTS的Token财产时,才应致电。否则,你应该调用它,因为它是一个比赛。例如,在这里:

https://github.com/aspnet/AspNetKatana/issues/108

在此问题的修复程序中,以前所做的代码cts.Cancel(); cts.Dispose();被编辑为仅执行此操作,cts.Cancel();因为不幸的是,任何人不幸地尝试获取取消令牌以便在调用Dispose 之后观察其取消状态都将需要处理ObjectDisposedException-除了在OperationCanceledException他们正计划进行。

Tratcher提出了与此修复相关的另一个关键观察结果:“仅对于不会被取消的令牌才需要进行处置,因为取消会进行所有相同的清理。” 即只做Cancel()而不是处理就足够了!


The*_*ias 5

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

Dispose只有在CancellationTokenSource对象上的所有其他操作都完成后才能使用该方法。

而且

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

这是课程:

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<bool> _completionSource;
        private bool _disposed;

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

        public Operation(CancellationTokenSource cts)
        {
            _cts = cts;
            _completionSource = new TaskCompletionSource<bool>(
                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(true); }
        }
    }

    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)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken);
        using (var operation = new Operation(cts))
        {
            // Set this as the active operation
            var oldOperation = Interlocked.Exchange(ref _activeOperation, operation);
            try
            {
                if (oldOperation != null && !_allowConcurrency)
                {
                    oldOperation.Cancel();
                    await oldOperation.Completion; // Continue on captured context
                    // The Completion never fails
                }
                cts.Token.ThrowIfCancellationRequested();
                var task = action(cts.Token); // Invoke on the initial context
                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 cts is disposed along with the operation
    }

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

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

    public bool Cancel() => CancelAsync() != Task.CompletedTask;
}
Run Code Online (Sandbox Code Playgroud)

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

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

private readonly CancelableExecution _cancelableExecution = new CancelableExecution();

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();
}
Run Code Online (Sandbox Code Playgroud)

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