确保异步任务在重新启动该任务之前已完全取消
好的,这里是:我有一个应用程序的一部分,用于查询数据库中的行。当用户在搜索框中输入文本时(或者改变另一个过滤器设置),我执行查询。确保异步任务在重新启动该任务之前已完全取消
从数据库返回的数据将进入绑定到DataGrid的ObservableCollection。因为我意识到保持UI响应,我使用Async-Await(尝试)在后台填充此ObservableCollection。我想要取消正在进行的任务等待它确认取消,然后“重新启动”(或更确切地说,创建一个新的任务)与新的设置。
但我得到各种奇怪的结果(尤其是当我减慢任务模拟慢数据库访问),如收集没有得到清除和两次填充和处置CancellationTokenSource(我读取的是一个好主意),有时当我打电话给Cancel()
时,它已被处置,同时我得到一个例外。
我怀疑这个问题源于我对这个模式的理解存在根本性的差距,因此任何样式/模式指针都像实际的技术解决方案一样受欢迎。
的代码基本上是这样的:
ObservableCollection<Thing> _thingCollection;
Task _thingUpdaterTask;
CancellationTokenSource _thingUpdaterCancellationSource;
// initialisation etc. here
async void PopulateThings(ThingFilterSettings settings)
{
// try to cancel any ongoing task
if(_thingUpdaterTask?.IsCompleted ?? false){
_thingUpdaterCancellationSource.Cancel();
await _thingUpdaterTask;
}
// I'm hoping that any ongoing task is now done with,
// but in reality that isn't happening. I'm guessing
// that's because Tasks are getting dereferenced and
// orphaned in concurrent calls to this method?
_thingCollection.Clear();
_thingUpdaterCancellationSource = new CancellationTokenSource();
var cancellationToken = _thingUpdaterCancellationSource.Token;
var progressHandler = new Progress<Thing>(x => _thingCollection.add(x));
var progress = (IProgress<Thing>)progressHandler;
try{
_thingUpdaterTask = Task.Factory.StartNew(
() => GetThings(settings, progress, cancellationToken));
await _thingUpdaterTask;
}catch(AggregateException e){
//handle stuff etc.
}finally{
// should I be disposing the Token Source here?
}
}
void GetThings(ThingFilterSettings settings,
IProgress<Thing> progress,
CancellationToken ctok){
foreach(var thingy in SomeGetThingsMethod(settings)){
if(ctok.IsCancellationRequested){
break;
}
progress.Report(thingy);
}
}
你可以开始新任务前加一个包装类,将等待前一个任务停止执行(无论是通过完成或通过取消)。
public class ChainableTask
{
private readonly Task _task;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public ChainableTask(Func<CancellationToken, Task> asyncAction,
ChainableTask previous = null)
{
_task = Execute(asyncAction, previous);
}
private async Task CancelAsync()
{
try
{
_cts.Cancel();
await _task;
}
catch (OperationCanceledException)
{ }
}
private async Task Execute(Func<CancellationToken, Task> asyncAction, ChainableTask previous)
{
if (previous != null)
await previous.CancelAsync();
if (_cts.IsCancellationRequested)
return;
await asyncAction(_cts.Token);
}
}
如果在以前的项目中使用上面的类。这个班级需要一个lambda,asyncAction
来创建下一个任务。该任务仅在前一个完成后创建。
它会将CancellationToken
传递给每个任务,以便在完成前停止任务。在开始下一个任务之前,前一个任务的令牌被取消,并且前一个任务正在等待。这发生在CancelAsync
。
只有等到之前的Cancel
后,我们才会调用lambda来创建下一个任务。
的利用方法:
var firstAction = new ChainableTask(async tcs => await Task.Delay(1000));
var secondAction = new ChainableTask(async tcs => await Task.Delay(1000), firstAction); // pass the previous action
在这个例子中,所创建的任务不支持取消,所以ChainableTask
第二个电话会等到第一Task.Delay(1000)
完成后,拨打第二个之前。
'await'展开异常,无需捕获'AggregateException'。 – xxbbcc
@dymanoid - 当我尝试推广我的代码的例子时,我犯了一个错字。修复并希望现在有意义! – LexyStardust
@LexyStardust不使用'async void',除了事件处理程序。你应该更新该方法返回'任务' – Nkosi