正确取消和清除 CancellationToken

Correctly cancelling and clearing a CancellationToken

我们有一个名为 GetThingsasync 方法,它会发送给 4 个不同的提供者并寻找结果。其中两个提供程序非常快,因此不是异步编写的,但是另外两个提供程序很慢并且是异步编写的。

在文本更改时调用 GetThings 方法,因此调用它应该会取消之前的调用。我们使用 CancellationTokenSource 来执行此操作。 GetThings 方法的开头如下所示:

private async void GetThings()
{
    if (this.quickEntryCancellationTokenSource != null &&
        this.quickEntryCancellationTokenSource.IsCancellationRequested == false)
    {
        this.quickEntryCancellationTokenSource.Cancel();
    }

    this.quickEntryCancellationTokenSource = new CancellationTokenSource();

我们决定在所有 4 个提供程序完成之前从 GetThings 返回任何内容都没有意义。我们正在使用 Task.WhenAll

完成此操作
var getThings1Task = this.GetThings1();
var getThings2Task = this.GetThings2();
var getThings3Task = this.GetThings3();
var getThings4Task = this.GetThings4();

await Task.WhenAll(getThings1Task, getThings2Task, getThings3Task, getThings4Task).ConfigureAwait(false);

// Read the .Result of each of the tasks

localSuggestions.Insert(0, getThings1Tasks.Result);
localSuggestions.Insert(1, getThings2Tasks.Result);
localSuggestions.Insert(2, getThings3Tasks.Result);
localSuggestions.Insert(3, getThings4Tasks.Result);

this.quickEntryCancellationTokenSource = null;

读取每个任务的 .Result 后,我​​们将 quickEntryCancellationTokenSource 设置为 null。所有这些代码都包含在通常的 CancellationTokenSource 异常处理中。如有必要,GetThings3 的开始将 CancellationTokenSource 向下传递一层。

private async Task<GroupedResult<string, SearchSuggestionItemViewModel>> GetThings3()
{
    List<Code> suggestions;

    if (this.SearchTerm.Length >= 3)
    {
        suggestions = await this.provider3.SearchAsync(this.SearchTerm, this.quickEntryCancellationTokenSource.Token);
    }
    else
    {
        suggestions = new List<Code>();
    }

我们看到的问题是有时 quickEntryCancellationTokenSource 为 null。在我的天真中,我看不出这是怎么可能的,因为 CancellationTokenSource 是在等待任务之前创建的并且没有设置为 Null 直到他们完成。

我的问题是:

  1. quickEntryCancellationTokenSource怎么为空?
  2. 我们应该同步对 cancellationTokenSource 的访问吗? 因为四个供应商中的两个太快了,他们从不处理 cancellationTokenSource。
  3. 所有四种方法都应该调用 ThrowIfCancellationRequested 吗?
  4. 四个提供程序方法中的两个从不等待任何东西是错误的吗?

Should we be synchronising access to the cancellationTokenSource? Because two of the four providers are so quick they never deal with the cancellationTokenSource.

您应该同步对 CTS 的访问,但不是出于这个原因。原因是因为 quickEntryCancellationTokenSource 字段可以从多个线程访问。

如果这是在 UI 线程的上下文中,那么您可以只使用 UI 线程进行同步:

// No ConfigureAwait(false); we want to only access the CTS from the UI thread.
await Task.WhenAll(getThings1Task, getThings2Task, getThings3Task, getThings4Task);
...
this.quickEntryCancellationTokenSource = null;

此外,我会考虑将 CT 传递给 getThings1Task 和其他人,而不是让他们从私有变量中读取。

Should all four methods be calling ThrowIfCancellationRequested?

这取决于你。如果您的两个 "fast" 提供程序方法不采用 CT,那么您 可以 检查它,但在我看来它不会提供任何好处。

Is it wrong that two of the four provider methods never await anything?

如果他们正在做 I/O,他们应该使用 await,即使他们 通常 非常快。如果他们不做 I/O,那么我认为将它们包装在 Task 中根本没有多大意义。据推测,当前代码是同步执行的,并且总是 returns 一个已完成的任务,所以它只是一种更复杂的同步执行代码的方式。 OTOH,如果他们正在做 I/O 但你没有他们的异步 API (并且因为你在 UI 上下文中),你可以将它们包装在 Task.Run.

Once we have read the .Result of each of the Task

考虑使用 await,即使是已完成的任务。它将使您的异常处理代码更容易。

我对你的问题的回答是:

  1. 没有。 CancellationTokenSource is thread-safe,但对字段本身的访问应该是同步的。我有两个建议来避免这个问题:

首先,最好直接将CancellationToken传递给GetThings3,而不是引用全局变量:

private async Task<GroupedResult<string, SearchSuggestionItemViewModel>> GetThings3(CancellationToken token) { ... }    

var getThings3Task = this.GetThings3(quickEntryCancellationTokenSource.Token);

其次,最好避免访问外部变量。我的建议是使用 local CancellationTokenSource:

using (var localTokenSource = new CancellationTokenSource())
{
    var getThings1Task = this.GetThings1(localTokenSource.Token);
    ...
    await Task.WhenAll(...);
}

如果您想创建一个与全球令牌源链接的令牌,您可能还需要考虑 CancellationTokenSource.CreateLinkedTokenSource(...)

  1. 是的,最好按照之前的建议打电话给
token.ThrowIfCancellationRequested();

但这在非 CPU 绑定的用户代码中通常没有必要。

  1. 没问题:他们 return 已经完成了任务。