196

该类CancellationTokenSource是一次性的。快速浏览一下 Reflector 就证明KernelEvent了(很可能)非托管资源的使用。由于CancellationTokenSource没有终结器,如果我们不处置它,GC 就不会这样做。

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

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

  1. using如果您不等待,则无法包装开始并行任务的代码。只有在您不等待的情况下取消才有意义。
  2. 当然,您可以ContinueWith通过调用添加任务Dispose,但这是要走的路吗?
  3. 可取消的 PLINQ 查询不同步回来,但只是在最后做一些事情呢?比方说.ForAll(x => Console.Write(x))
  4. 它可以重复使用吗?是否可以将同一个令牌用于多个调用,然后将其与宿主组件一起处置,比如 UI 控件?

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

4

7 回答 7

98

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

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

MSDN Cancellation in Managed Threads清楚地说明了这一点:

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

ContinueWith在我的实现中使用过。

于 2012-09-18T10:05:07.470 回答
60

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

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

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

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

于 2015-06-15T21:45:14.643 回答
31

您应该始终处置CancellationTokenSource.

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

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

  2. 使用任务时,请使用ContinueWith您指定的任务来处理CancellationTokenSource.

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

  4. 对于 UI,您可以CancellationTokenSource为每个不与单个取消触发器绑定的可取消操作创建一个新的。维护一个List<IDisposable>并将每个源添加到列表中,当您的组件被释放时将它们全部释放。

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

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

于 2013-08-26T13:50:56.653 回答
28

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

于 2011-08-05T18:54:08.997 回答
25

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

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

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

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

于 2014-07-06T10:28:58.437 回答
23

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

CancellationTokenSource.Dispose()只有当您确定没有人会试图获得 CTS 的Token财产时,您才应该打电话。否则你不应该打电话Dispose()因为它会产生竞争条件。例如,请参见此处:

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

在此问题的修复中,之前所做的代码cts.Cancel(); cts.Dispose();被编辑为只是做,cts.Cancel();因为不幸的是,任何不幸地试图获取取消令牌以在被调用 Dispose观察其取消状态的人也需要处理ObjectDisposedException- 除了OperationCanceledException他们计划的。

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

于 2018-07-07T05:10:31.443 回答
7

我创建了一个线程安全的类,将 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;
}

类的主要方法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();
}

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

于 2020-05-08T14:54:55.030 回答