2017-09-13 28 views
2

好的,这里是:我有一个应用程序的一部分,用于查询数据库中的行。当用户在搜索框中输入文本时(或者改变另一个过滤器设置),我执行查询。确保异步任务在重新启动该任务之前已完全取消

从数据库返回的数据将进入绑定到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); 
    } 
} 
+2

'await'展开异常,无需捕获'AggregateException'。 – xxbbcc

+0

@dymanoid - 当我尝试推广我的代码的例子时,我犯了一个错字。修复并希望现在有意义! – LexyStardust

+2

@LexyStardust不使用'async void',除了事件处理程序。你应该更新该方法返回'任务' – Nkosi

回答

0

你可以开始新任务前加一个包装类,将等待前一个任务停止执行(无论是通过完成或通过取消)。

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)完成后,拨打第二个之前。