何时处置CancellationTokenSource?


163

该课程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实例上处理的正确和推荐策略是什么?

Answers:


81

谈论是否真的有必要调用Dispose on CancellationTokenSource...我的项目中发生内存泄漏,CancellationTokenSource结果就是问题所在。

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

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

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

ContinueWith在实现中使用过。


14
这是Bryan Crosby当前接受的答案中的一个重要遗漏-如果创建链接的 CTS,则存在内存泄漏的风险。该方案与永远不会取消注册的事件处理程序非常相似。
索伦Boisen

5
由于相同的问题,我发生了泄漏。使用探查器,我可以看到回调注册包含对链接的CTS实例的引用。在这里检查CTS Dispose实现的代码非常有见地,并强调了@SørenBoisen与事件处理程序注册泄漏的比较。
BitMask777 '16

上面的评论反映了讨论的状态,@ Bryan Crosby的其他回答被接受。
乔治·马玛拉兹

在2020年的文件明确表示:Important: The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.- docs.microsoft.com/en-us/dotnet/standard/threading/...
Endrju

44

我认为当前的答案都不令人满意。经过研究,我从Stephen Toub(参考)找到了以下答复:

这取决于。在.NET 4中,CTS.Dispose有两个主要用途。如果已经访问了CancellationToken的WaitHandle(因此延迟分配了它),则Dispose将处理该句柄。此外,如果CTS是通过CreateLinkedTokenSource方法创建的,则Dispose将取消CTS与它链接到的令牌的链接。在.NET 4.5中,Dispose还有另外一个用途,即如果CTS在后台使用了Timer(例如,调用了CancelAfter),则将废弃该Timer。

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

我认为大胆的部分是重要的部分。他使用“更具影响力”,这使它含糊不清。我将其解释为意味着Dispose应在这些情况下进行调用,否则Dispose无需使用。


10
更具影响力意味着将子级CTS添加到父级CTS中。如果您不处置孩子,那么如果父母长寿,将有泄漏的可能。因此,处理链接的链接至关重要。
Grigory

26

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


7
我有同样的感觉,GC将清除所有内容。我将尝试验证。为什么Microsoft在这种情况下实施处置?为了摆脱事件回调并避免传播到第二代GC。在这种情况下,调用Dispose是可选的-如果可以的话调用它,如果不忽略它的话。我认为不是最好的方式。
乔治·马玛拉兹

4
我已经研究了这个问题。CancellationTokenSource获取垃圾回收。您可能会有助于在GEN 1 GC中进行处理。公认。
乔治·马马拉兹

1
我独立进行了同样的调查,并得出了相同的结论:如果可以的话,请进行处置,但不要在这种罕见但并非闻所未闻的情况下烦恼,因为您已经将CancellationToken发送到弓箭手,不想等他们回写明信片告诉您他们已经完成了。由于使用CancellationToken的性质,这种情况会不时发生,我保证,这确实可以。
Joe Amenta 2015年

6
我的上述评论不适用于链接令牌源;我无法证明没有这些问题是可以的,并且该线程和MSDN的智慧表明,事实并非如此。
Joe Amenta 2015年

23

你应该总是处置CancellationTokenSource

如何处置它完全取决于方案。您提出了几种不同的方案。

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

  2. 使用任务时,请ContinueWith按指示使用任务处理CancellationTokenSource

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

  4. 对于UI,您可以CancellationTokenSource为每个可取消的操作创建一个新对象,而该新操作不受单个取消触发器的约束。维护一个List<IDisposable>并将每个源添加到列表中,并在处置组件时将其全部处置。

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

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


对于第2点,有什么原因您不能await在任务上使用,并在等待之后的代码中处理CancellationTokenSource?
stijn 2014年

14
有一些警告。如果您await在操作过程中取消了CTS ,则可能会因退出而继续操作OperationCanceledException。然后,您可以致电Dispose()。但是,如果仍在运行某些操作并使用CancellationTokenCanBeCanceledtrue即使已处理源,该令牌仍会报告为存在。如果他们尝试注册取消回调,BOOM!ObjectDisposedExceptionDispose()成功完成操作后,调用它是足够安全的。当您实际上需要取消某些操作时,它变得非常棘手
2014年

8
由于Mike Strobel给出的原因而被否决-由于CTS和Task具有异步特性,因此强制始终调用Dispose的规则会使您陷入毛病。该规则应改为:始终处置链接的令牌源。
索伦Boisen

1
您的链接指向已删除的答案。
Trisped

19

这个答案仍会出现在Google搜索中,我相信投票后的答案并不能完整地说明问题。在查看了(CTS)和(CT)的源代码之后,我相信在大多数情况下,以下代码序列很好:CancellationTokenSourceCancellationToken

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

m_kernelHandle上面提到的内部字段是支持WaitHandleCTS和CT类中的属性的同步对象。仅当您访问该属性时才实例化它。因此,除非您WaitHandleTask调用处置中使用了一些老式的线程同步,否则不会起作用。

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


7
MSDN文章“ 托管线程中的取消 ”指出:“侦听器IsCancellationRequested通过轮询,回调或等待句柄监视令牌属性的值。” 换句话说:使用等待句柄的可能不是(即发出异步请求的人),而是监听器(即响应请求的人)。这意味着,作为负责处理的人,您实际上无法控制是否使用了等待句柄。
herzbube15年

根据MSDN,注册的回调异常将导致.Cancel引发。如果发生这种情况,您的代码将不会调用.Dispose()。回调应注意不要这样做,但是可能会发生。
Joseph Lennox

11

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

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

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

在修复此问题,代码以前没有cts.Cancel(); cts.Dispose();被编辑只是做cts.Cancel();,因为任何人都这么倒霉的,试图让以观察它的取消状态令牌取消后, Dispose一直被称为将遗憾的是还需要处理ObjectDisposedException-除OperationCanceledException他们正在计划。

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


1

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

Dispose仅当CancellationTokenSource对象上的所有其他操作都已完成时才可以使用该方法。

而且

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

这是课程:

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

    private class Operation : IDisposable
    {
        private readonly object _locker = new object();
        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 (_locker) if (!_disposed) _cts.Cancel();
        }
        void IDisposable.Dispose() // Is called only once
        {
            try
            {
                lock (_locker) { _cts.Dispose(); _disposed = true; }
            }
            finally { _completionSource.SetResult(true); }
        }
    }

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

    public bool IsRunning =>
        Interlocked.CompareExchange(ref _activeOperation, null, null) != null;

    public async Task<TResult> RunAsync<TResult>(
        Func<CancellationToken, Task<TResult>> taskFactory,
        CancellationToken extraToken = default)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken, default);
        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
                }
                var task = taskFactory(cts.Token); // Run in 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);
            }
        }
    }

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

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

    public bool Cancel() => CancelAsync() != Task.CompletedTask;
}

CancelableExecution该类的主要方法是RunAsyncCancel。默认情况下,不允许并发操作,这意味着调用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();
}

RunAsync方法接受一个额外的CancellationToken作为参数,该参数链接到内部创建的CancellationTokenSource。提供此可选令牌可能对提前使用场景有用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.