同时等待具有独立延续的多个异步调用

时间:2014-04-28 17:51:04

标签: c# task-parallel-library async-await

有几种情况我需要调用多个异步调用(来自同一个事件处理程序),这些异步调用可以彼此独立地进行,每个异步调用都有自己的更新UI的延续。

以下天真实现会导致三个异步操作按顺序执行:

private async void button_Click(object sender, RoutedEventArgs e)
{
    nameTextBox.Text = await GetNameAsync();
    cityTextBox.Text = await GetCityAsync();
    rankTextBox.Text = await GetRankAsync();
}

有一个MSDN example建议将任务的创建与各自的await语句分开,允许它们并行运行:

private async void button_Click(object sender, RoutedEventArgs e)
{
    var nameTask = GetNameAsync();
    var cityTask = GetCityAsync();
    var rankTask = GetRankAsync();

    nameTextBox.Text = await nameTask;
    cityTextBox.Text = await cityTask;
    rankTextBox.Text = await rankTask;
}

但是,这种方法的局限性在于任务延续仍然按顺序注册,这意味着 n th 延续不能在其前面的所有 n -1连续已完成,即使其任务可能是第一个完成。

使异步任务并行运行的最佳模式是什么,但是一旦各自的任务完成,每个延续都会运行?

修改:大多数答案建议等待Task.WhenAll,如下所示:

private async void button_Click(object sender, RoutedEventArgs e)
{
    var nameTask = GetNameAsync(); 
    var cityTask = GetCityAsync(); 
    var rankTask = GetRankAsync(); 

    await Task.WhenAll(nameTask, cityTask, rankTask);

    nameTextBox.Text = nameTask.Result;
    cityTextBox.Text = cityTask.Result;
    rankTextBox.Text = rankTask.Result;
}

然而,这不符合我的要求,因为我需要在每个任务完成后立即执行每个继续。例如,假设GetNameAsync需要5s,GetCityAsync需要2s,GetRankAsync需要8s。上面的实现会导致所有三个文本框仅在8s后更新,即使nameTextBoxcityTextBox的结果早得知道。

4 个答案:

答案 0 :(得分:3)

传统方法是使用ContinueWith为每个异步任务注册相应的延续:

private async void button_Click(object sender, RoutedEventArgs e)
{
    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.WhenAll(
        GetNameAsync().ContinueWith(nameTask => { nameTextBox.Text = nameTask.Result; }, uiScheduler),
        GetCityAsync().ContinueWith(cityTask => { cityTextBox.Text = cityTask.Result; }, uiScheduler),
        GetRankAsync().ContinueWith(rankTask => { rankTextBox.Text = rankTask.Result; }, uiScheduler));
}

使用C#5,现在最好使用await-style模式。通过将每个任务延续对分成自己的方法,可以最容易地实现这一点:

private async void button_Click(object sender, RoutedEventArgs e)
{
    await Task.WhenAll(
        PopulateNameAsync(),
        PopulateCityAsync(),
        PopulateRankAsync());
}

private async Task PopulateNameAsync()
{
    nameTextBox.Text = await GetNameAsync();
}

private async Task PopulateCityAsync()
{
    cityTextBox.Text = await GetCityAsync();
}

private async Task PopulateRankAsync()
{
    rankTextBox.Text = await GetRankAsync();
}

定义所有这些简单的方法很快变得很麻烦,所以可以将它们压缩成异步lambda:

private async void button_Click(object sender, RoutedEventArgs e)
{
    await Task.WhenAll(
        new Func<Task>(async () => { nameTextBox.Text = await GetNameAsync(); })(),
        new Func<Task>(async () => { cityTextBox.Text = await GetCityAsync(); })(),
        new Func<Task>(async () => { rankTextBox.Text = await GetRankAsync(); })());
}

如果经常使用这种模式,定义一个可以接受Func<Task> lambdas并执行它们的实用程序方法也会很有帮助,使我们的事件处理程序代码更简洁和可读:

public static Task WhenAllTasks(params Func<Task>[] taskProducers)
{
    return Task.WhenAll(taskProducers.Select(taskProducer => taskProducer()));
}

private async void button_Click(object sender, RoutedEventArgs e)
{
    await WhenAllTasks(
        async () => nameTextBox.Text = await GetNameAsync(),
        async () => cityTextBox.Text = await GetCityAsync(),
        async () => rankTextBox.Text = await GetRankAsync());
}

答案 1 :(得分:3)

更简单的替代方案是:

private async void button_Click(object sender, RoutedEventArgs e)
{
    var results = await Task.WhenAll(
        GetNameAsync(),
        GetCityAsync(),
        GetRankAsync()
    );

    nameTextBox.Text = results[0];
    nameCityBox.Text = results[1];
    nameRankBox.Text = results[2];
}

没有关闭,没有额外的状态机。

答案 2 :(得分:1)

据我了解,您需要查询三个异步资源,并且只有在返回三个异步资源后才更新UI。如果这是正确的Task.WhenAll控件是想要使用。像

这样的东西
private async void button_Click(object sender, RoutedEventArgs e)
{
    string nametext = null;
    string citytext = null;
    string ranktext = null;

    await Task.WhenAll(
        async () => nametext = await GetNameAsync(),
        async () => citytext = await GetCityAsync(),
        async () => ranktext = await GetRankAsync()
    );

    nameTextBox.Text = nametext;
    nameCityBox.Text = citytext;
    nameRankBox.Text = ranktext;
}

答案 3 :(得分:0)

private async void button_Click(object sender, RoutedEventArgs)
{ 
    var nameTask = GetNameAsync();
    var cityTask= GetCityAsync();
    var rankTask= GetRankAsync();

    System.Threading.Tasks.Task.WaitAll(nameTask, cityTask, rankTask);

    nameTextBox.Text = nameTask.Result;
    cityTextBox.Text = cityTask.Result;
    rankTextBox.Text = rankTask.Result;
}

更多详情:https://msdn.microsoft.com/pt-br/library/dd270695(v=vs.110).aspx