如何在多个任务之后继续而不阻塞 UI 线程?
Posted
技术标签:
【中文标题】如何在多个任务之后继续而不阻塞 UI 线程?【英文标题】:How do I continue after multiple Tasks without blocking the UI thread? 【发布时间】:2013-08-08 03:28:06 【问题描述】:在我的 MVVM 应用程序中,我的视图模型调用 3 种不同的服务方法,将每个方法的数据转换为通用格式,然后使用属性通知/可观察集合等更新 UI。
服务层中的每个方法都会启动一个新的Task
,并将Task
返回给视图模型。这是我的一种服务方法的示例。
public class ResourceService
internal static Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> 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<DataItem> Data = new ObservableCollection<DataItem>();
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 并显示 BusyIndicator 控件,但它不起作用。我还尝试添加简单的字符串属性并绑定它们,它们也没有被更新。 IsBusy 功能是基类的一部分,适用于其他视图模型,在这些视图模型中我没有运行多个任务,因此我不认为 XAML 中的属性通知或数据绑定存在问题。
在整个方法完成后,所有数据绑定似乎都已更新。我在输出窗口中没有看到任何“第一次异常”或绑定错误,这让我相信 UI 线程在调用 WaitAll 之前被阻塞了。
第 2 点:我似乎从服务方法返回了错误的任务。我希望WaitAll
之后的所有内容都在视图模型转换了回调中所有服务方法的所有结果之后运行。但是,如果我从服务方法返回延续任务,则永远不会调用延续,WaitAll
将永远等待。奇怪的是绑定到 ICollectionView 的 UI 控件实际上正确显示了所有内容,我认为这是因为 Data 是一个可观察的集合,而 CollectionViewSource 知道集合更改的事件。
【问题讨论】:
【参考方案1】:您可以使用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;
但是,如果我从服务方法返回延续任务,则永远不会调用延续,WaitAll 将永远等待
问题是您的延续任务需要 UI 线程,而您在 WaitAll
调用中阻塞了 UI 线程。这会造成无法解决的死锁。
修复上述问题应该可以纠正这个问题 - 您需要将继续作为任务返回,因为这是您需要等待完成的内容 - 但是通过使用 TaskFactory.ContinueWhenAll
您可以释放 UI 线程,以便它可以处理这些继续.
请注意,这是使用 C# 5 简化的另一件事。您可以将其他方法编写为:
internal static async Task LoadResources(Action<IEnumerable<Resource>> completedCallback, Action<Exception> errorCallback)
try
await Task.Run(() =>
//... get resources from somewhere
return resources;
);
catch (Exception e)
errorCallback(task.Exception);
completedCallback(task.Result);
话虽如此,通常最好编写方法以返回 Task<T>
而不是提供回调,因为这样可以简化两端的使用。
【讨论】:
我一直在推迟使用 async/await,因为我不确定它是否与 WPF 命令和 Prism DelegateCommand 一起“正常工作”。 Task.ContinueWhenAll 对我来说似乎不存在,我需要引用一些 TPL 扩展库吗? @BenCr 抱歉 - 这是 TaskFactory.ContinueWhenAll。一般来说,等待/异步的东西比 WPF 的任务延续工作更好。主要区别在于您不必为了处理异常而费尽心思。 感谢 Reed,这似乎工作得更好。希望有人能够在调用 WaitAll 之前为阻塞提供解释,但这肯定解决了问题 1 和 2。我将在下一次迭代中尝试 async/await 的内容,看看我是如何进行的。跨度> @BenCR 我刚刚编辑了我的帖子来解释它。基本上,WaitAll 会阻塞 UI,直到任务完成,但任务需要 UI 才能完成;) @BenCr:Async/await 比使用带有延续的任务(以及来自同步上下文的任务调度程序等)要容易得多。如果你需要async
命令,你可以使用AsyncDelegateCommand
from here。以上是关于如何在多个任务之后继续而不阻塞 UI 线程?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 executor.execute 和 future.get() 结束任务(使线程超时)(通过上升中断)而不阻塞主线程