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

时间:2022-11-10 20:56:40

In my MVVM application my view model calls 3 different service methods, converts the data from each into a common format and then updates the UI using property notification/observable collections etc.

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

Each method in the service layer starts a new Task and returns the Task to the view model. Here's an example of one of my service methods.

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

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;
}
}

Here's the calling code and other relevant parts of the view model...

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

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
    });
}

This almost works but there are a couple of small issues.

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

Number 1: In the call to SetBusy at the very beginning, before I start any tasks and before I call WaitAll, I set the IsBusy property to true. This should update the UI and show the BusyIndicator control but it's not working. I've also tried adding simple string properties and binding those and they're not being updated either. The IsBusy functionality is part of a base class and works in other view models where I don't have more than one Task running so I don't believe there is an issue with the property notification or data binding in the XAML.

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

All the data bindings seem to be updated after the whole method has completed. I'm not seeing any "first time exceptions" or binding errors in the output Window which is leading me to believe the UI thread is somehow being blocked before the call to WaitAll.

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

Number 2: I seem to be returning the wrong Tasks from the service methods. I want everything after WaitAll to run after the view model has converted all the results from all the service methods in the callbacks. However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever. The strange thing is the UI control bound to the ICollectionView actually displays everything correctly, I assumed this is because Data is an observable collection and the CollectionViewSource is aware of the collection changed events.

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

1 个解决方案

#1


9  

You can use TaskFactory.ContinueWhenAll to build a continuation that runs when the input tasks all complete.

您可以使用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());

Note that this becomes simpler if you use C# 5's await/async syntax:

请注意,如果您使用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;
}

However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever

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

The problem is that your continuation task requires the UI thread, and you're blocking the UI thread in the WaitAll call. This creates a deadlock which will not resolve.

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

Fixing the above should correct this - you'll want to return the Continuation as the Task, as that's what you need to wait for completion - but by using TaskFactory.ContinueWhenAll you free up the UI thread so it can process those continuations.

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

Note that this is another thing that gets simplified with C# 5. You can write your other methods as:

请注意,这是使用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);
}

That being said, it's typically better to write the methods to return a Task<T> instead of providing callbacks, as that simplifies both ends of the usage.

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

#1


9  

You can use TaskFactory.ContinueWhenAll to build a continuation that runs when the input tasks all complete.

您可以使用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());

Note that this becomes simpler if you use C# 5's await/async syntax:

请注意,如果您使用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;
}

However if I return the continuation task from the service method the continuation never gets called and WaitAll waits forever

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

The problem is that your continuation task requires the UI thread, and you're blocking the UI thread in the WaitAll call. This creates a deadlock which will not resolve.

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

Fixing the above should correct this - you'll want to return the Continuation as the Task, as that's what you need to wait for completion - but by using TaskFactory.ContinueWhenAll you free up the UI thread so it can process those continuations.

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

Note that this is another thing that gets simplified with C# 5. You can write your other methods as:

请注意,这是使用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);
}

That being said, it's typically better to write the methods to return a Task<T> instead of providing callbacks, as that simplifies both ends of the usage.

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