何时处置CancellationTokenSource?

时间:2011-08-05 17:51:04

标签: c# c#-4.0 task-parallel-library plinq parallel-extensions

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

另一方面,如果您查看MSDN文章Cancellation in Managed Threads中列出的示例,则只有一个代码段会丢弃该令牌。

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

  1. 如果您不等待,则无法使用using包装启动并行任务的代码。只有在你不等的时候取消才有意义。
  2. 当然,您可以通过ContinueWith电话在任务中添加Dispose,但这是要走的路吗?
  3. 可取消的PLINQ查询怎么样,它不会同步回来,但最后会做些什么?我们说.ForAll(x => Console.Write(x))
  4. 可重复使用吗?是否可以将相同的令牌用于多个调用,然后将其与主机组件一起处理,让我们说UI控件?
  5. 因为它没有Reset方法来清理IsCancelRequestedToken字段,所以我认为它不可重复使用,因此每次启动任务时(或者PLINQ查询)你应该创建一个新的。这是真的吗?如果是,我的问题是在这些Dispose个实例上处理CancellationTokenSource的正确和建议策略是什么?

7 个答案:

答案 0 :(得分:59)

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

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

MSDN Cancellation in Managed Threads明确指出:

  

请注意,完成后,您必须在链接的令牌源上调用Dispose。有关更完整的示例,请参阅How to: Listen for Multiple Cancellation Requests

我在实施中使用了ContinueWith

答案 1 :(得分:32)

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

  

这取决于。   在.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

答案 2 :(得分:25)

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

答案 3 :(得分:18)

您应该始终处置CancellationTokenSource

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

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

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

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

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

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

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

答案 4 :(得分:14)

这个答案仍在谷歌搜索中出现,我相信投票的答案并不是完整的故事。在查看CancellationTokenSource CancellationToken(CTS)和if (cancelTokenSource != null) { cancelTokenSource.Cancel(); cancelTokenSource.Dispose(); cancelTokenSource = null; } (CT)后,我相信对于大多数用例,以下代码序列都可以:

m_kernelHandle

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

当然,如果您 使用它,您应该执行上述其他答案所建议的内容,并延迟调用WaitHandle,直到使用句柄的任何{{1}}操作完成为止,因为,如source code中所述,结果未定义。

答案 5 :(得分:3)

问了很长时间以来,我得到了很多有用的答案,但是我遇到了一个与此相关的有趣问题,以为我会在这里将其作为其他答案:

仅当您确定没有人会尝试获取CTS的# create and format disk Get-Disk | Where PartitionStyle -eq 'Raw' | Select-Object -First 1 | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -DriveLetter F -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Containers" -Confirm:$false # move docker to F:\docker docker images -a -q | %{docker rmi $_ --force} Stop-Service Docker $service = (Get-Service Docker) $service.WaitForStatus("Stopped","00:00:30") @{"data-root"="F:\docker"} | ConvertTo-Json | Set-Content C:\programdata\docker\config\daemon.json Get-Process docker* | % {Stop-Process -Id $_.Id -Force} docker system info Copy-Item C:\programdata\docker F:\docker -Recurse Start-Service Docker 属性时,才应致电CancellationTokenSource.Dispose()。否则,您不应该调用它,因为这是一场比赛。例如,在这里:

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

在此问题的修复程序中,以前执行Token的代码被编辑为仅执行cts.Cancel(); cts.Dispose();,因为有人不幸地尝试获取取消令牌以观察其取消状态不幸的是,在调用之后,除了他们计划的cts.Cancel();之外,还需要处理ObjectDisposedException

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

答案 6 :(得分:2)

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

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

还有also

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。提供此可选令牌可能对提前使用场景很有帮助。