何时处理CancellationTokenSource?

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的正确和推荐策略是什么?

69815 次浏览

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

谈到是否真的有必要在CancellationTokenSource上调用Dispose…我在我的项目中有一个内存泄漏,事实证明CancellationTokenSource是问题所在。

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

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

注意,当你完成它时,你必须在链接的令牌源上调用Dispose。有关更完整的示例,请参见如何:监听多个取消请求

我在实现中使用了ContinueWith

你应该总是处理CancellationTokenSource

如何处置它完全取决于场景。你提出了几个不同的场景。

  1. using只在你正在等待的一些并行工作上使用CancellationTokenSource时有效。如果这是你的赛琳娜,那么很好,这是最简单的方法。

  2. 当使用任务时,使用ContinueWith任务来处理CancellationTokenSource

  3. 对于plinq,你可以使用using,因为你在并行运行它,但等待所有并行运行的worker完成。

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

  5. 对于线程,创建一个连接所有工作线程的新线程,并在所有工作线程完成时关闭单个源。见CancellationTokenSource,何时处置?< / >

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

这个答案仍然出现在谷歌的搜索中,我相信投票选出的答案并没有给出完整的故事。在查看了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文档中所描述的,结果是未定义的。

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

< p >视情况而定。 在。net 4中,CTS。Dispose有两个主要用途。如果 CancellationToken的WaitHandle已被访问(因此是懒惰的 分配它),Dispose将丢弃该句柄。另外,如果 CTS是通过CreateLinkedTokenSource方法创建的,Dispose 将解除CTS与它所链接到的令牌的链接。在。net 4.5中, Dispose还有一个额外的用途,即如果CTS使用Timer 在被罩下(例如CancelAfter被调用),计时器将 处理。< / p > 对于CancellationToken,这是非常罕见的。WaitHandle被使用, 所以在它之后清理通常不是使用Dispose的好理由。 但是,如果您正在使用CreateLinkedTokenSource创建CTS,或者 如果你使用CTS的计时器功能,它会更有影响力

.

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

我已经很久没有问过这个问题了,得到了很多有用的答案,但我遇到了一个与此相关的有趣问题,我想把它作为另一个答案张贴在这里:

只有在确定没有人试图获取CTS的Token属性时,才应该调用CancellationTokenSource.Dispose()。否则,你应该调用Dispose(),因为它创建了一个竞态条件。例如,看这里:

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

在这个问题的修复中,之前执行cts.Cancel(); cts.Dispose();的代码被编辑为只执行cts.Cancel();,因为任何不幸的人试图获得取消令牌以观察其取消状态 Dispose已被调用,不幸的是,除了他们计划的OperationCanceledException之外,还需要处理ObjectDisposedException

与此修复相关的另一个关键观察是由Tratcher提出的:“只需要对不会被取消的令牌进行处理,因为取消执行所有相同的清理工作。” 也就是说,只做Cancel()而不是dispose就足够了!< / p >

我写了一个线程安全类,它将CancellationTokenSource绑定到Task,并保证CancellationTokenSource将在其关联的Task完成时被释放。它使用锁来确保CancellationTokenSource在被释放期间或之后不会被取消。这是为了符合文档,它指出:

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

:

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框架兼容的版本,你可以查看这个答案的第三次修订