我们有一个名为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 直到他们完成。
我的问题是:
答案 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)
我对你的问题的答案是:
CancellationTokenSource
is thread-safe,但应同步访问字段本身。我有两个建议可以避免这个问题:首先,最好直接将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(...)
。
token.ThrowIfCancellationRequested();
但在非CPU绑定的用户代码中通常不需要这样做。