如何在不阻止UI线程的情况下继续执行多个任务?

在我的MVVM应用程序中,我的视图模型调用3种不同的服务方法,将每种数据转换为通用格式,然后使用属性通知/可观察集合等更新UI。

服务层中的每个方法都会启动一个新Task ,并将Task返回给视图模型。 这是我的一种服务方法的示例。

 public class ResourceService { internal static Task LoadResources(Action<IEnumerable> completedCallback, Action errorCallback) { var t = Task.Factory.StartNew(() => { //... get resources from somewhere return resources; }); t.ContinueWith(task => { if (task.IsFaulted) { errorCallback(task.Exception); return; } completedCallback(task.Result); }, TaskScheduler.FromCurrentSynchronizationContext()); return t; } } 

这是调用代码和视图模型的其他相关部分……

 private ObservableCollection Data = new ObservableCollection(); public ICollectionView DataView { get { return _dataView; } set { if (_dataView != value) { _dataView = value; RaisePropertyChange(() => DataView); } } } private void LoadData() { SetBusy("Loading..."); Data.Clear(); Task[] tasks = new Task[3] { LoadTools(), LoadResources(), LoadPersonel() }; Task.WaitAll(tasks); DataView = CollectionViewSource.GetDefaultView(Data); DataView.Filter = FilterTimelineData; IsBusy = false; } private Task LoadResources() { return ResourceService.LoadResources(resources => { foreach(var r in resources) { var d = convertResource(r); Data.Add(d); } }, error => { // do some error handling }); } 

这几乎可行,但有一些小问题。

数字1:在一开始的SetBusy调用中,在我开始任何任务之前和调用WaitAll之前,我将IsBusy属性设置为true。 这应该更新UI并显示BusyIndi​​cator控件,但它不起作用。 我也尝试添加简单的字符串属性并绑定它们,它们也没有更新。 IsBusyfunction是基类的一部分,适用于其他视图模型,我没有运行多个任务,因此我不认为XAML中的属性通知或数据绑定存在问题。

整个方法完成后,所有数据绑定似乎都会更新。 我没有在输出窗口中看到任何“第一次exception”或绑定错误,这导致我相信在调用WaitAll之前UI线程以某种方式被阻止。

2号:我似乎从服务方法返回了错误的任务。 在视图模型转换了回调中所有服务方法的所有结果之后,我想在WaitAll之后运行所有内容。 但是,如果我从service方法返回continuation任务,则永远不会调用continuation并且WaitAll永远等待。 奇怪的是绑定到ICollectionView的UI控件实际上正确显示了所有内容,我假设这是因为Data是一个可观察的集合,而CollectionViewSource知道集合已更改的事件。

您可以使用TaskFactory.ContinueWhenAll构建一个在输入任务全部完成时运行的延续。

 Task[] tasks = new Task[3] { LoadTools(), LoadResources(), LoadPersonel() }; Task.Factory.ContinueWhenAll(tasks, t => { DataView = CollectionViewSource.GetDefaultView(Data); DataView.Filter = FilterTimelineData; IsBusy = false; }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); 

请注意,如果您使用C#5的await / async语法,这会变得更简单:

 private async void LoadData() { SetBusy("Loading..."); Data.Clear(); Task[] tasks = new Task[3] { LoadTools(), LoadResources(), LoadPersonel() }; await Task.WhenAll(tasks); DataView = CollectionViewSource.GetDefaultView(Data); DataView.Filter = FilterTimelineData; IsBusy = false; } 

但是,如果我从service方法返回continuation任务,则永远不会调用continuation并且WaitAll将永远等待

问题是您的延续任务需要UI线程,并且您在WaitAll调用中阻止了UI线程。 这会产生无法解决的死锁。

修复上述内容应该纠正这一点 – 你需要将Continuation作为任务返回,因为这是你需要等待完成的东西 – 但是通过使用TaskFactory.ContinueWhenAll你释放UI线程以便它可以处理这些延续。

请注意,这是使用C#5简化的另一件事。您可以将其他方法编写为:

 internal static async Task LoadResources(Action> completedCallback, Action errorCallback) { try { await Task.Run(() => { //... get resources from somewhere return resources; }); } catch (Exception e) { errorCallback(task.Exception); } completedCallback(task.Result); } 

话虽如此,通常最好编写方法来返回Task而不是提供回调,因为这简化了使用的两端。