正确取消和清除CancellationToken

时间:2017-03-27 12:58:10

标签: c# asynchronous async-await cancellationtokensource

我们有一个名为GetThings的{​​{1}}方法,该方法适用于4个不同的提供商并查找结果。其中两个提供程序非常快,因此不是异步写入的,但是其他两个提供程序很慢并且是异步写入的。

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

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

    this.quickEntryCancellationTokenSource = new CancellationTokenSource();

我们认为从GetThings返回任何内容都没有意义,直到所有4个提供商都完成了。我们正在使用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;

一旦我们读完了每个Task的.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为空。在我的天真中,我无法看到这是如何可能的,因为在等待任务之前创建了CancellationTokenSource并且未设置为Null 直到他们完成。

我的问题是:

  1. quickEntryCancellationTokenSource如何为null?
  2. 我们应该同步对cancellationTokenSource的访问吗? 因为四个提供商中有两个是如此之快,他们从不处理 cancelTokenSource。
  3. 所有四种方法都应该调用ThrowIfCancellationRequested吗?
  4. 四种提供者方法中的两种从未等待任何东西是错误的吗?

2 个答案:

答案 0 :(得分:2)

  

我们应该同步对cancellationTokenSource的访问吗?因为四个提供者中有两个如此之快,所以他们从不处理cancelTokenSource。

您应该同步对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和其他人,而不是让他们从私有变量中读取。

  

所有四种方法都应该调用ThrowIfCancellationRequested吗?

这取决于你。如果你的两个“快速”提供者方法不接受CT,那么你可以检查它,但它听起来不像我会提供任何好处。

  

四种提供者方法中的两种从未等待任何东西是错误的吗?

如果他们正在进行I / O,他们应该使用await,即使他们通常非常快。如果他们没有进行I / O操作,那么我认为将它们包装在Task中并不是很重要。据推测,当前代码同步执行并始终返回已完成的任务,因此它只是一种更复杂的方式,即首先同步执行代码。 OTOH,如果他们正在进行I / O但您没有异步API(并且因为您处于UI上下文中),您可以将它们包装在Task.Run中。

  

一旦我们阅读了每个任务的.Result

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

答案 1 :(得分:0)

我对你的问题的答案是:

  1. 否。 CancellationTokenSource is thread-safe,但应同步访问字段本身。我有两个建议可以避免这个问题:
  2. 首先,最好直接将CancellationToken传递给GetThings3,而不是引用全局变量:

    private async Task<GroupedResult<string, SearchSuggestionItemViewModel>> GetThings3(CancellationToken token) { ... }    
    
    var getThings3Task = this.GetThings3(quickEntryCancellationTokenSource.Token);
    

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

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

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

    1. 是的,最好按照之前的建议并致电
    2. token.ThrowIfCancellationRequested();
      

      但在非CPU绑定的用户代码中通常不需要这样做。

      1. 这里没问题:他们返回已经完成的任务。