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

Ben*_*nCr 5 c# wpf mvvm task-parallel-library busyindicator

在我的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;
}
}
Run Code Online (Sandbox Code Playgroud)

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

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
    });
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Ree*_*sey 9

您可以使用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());
Run Code Online (Sandbox Code Playgroud)

请注意,如果您使用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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

修复上述内容应该纠正这一点 - 你需要将Continuation作为任务返回,因为这是你需要等待完成的东西 - 但是通过使用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);
}
Run Code Online (Sandbox Code Playgroud)

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