Concurrently awaiting multiple asynchronous calls

2019-02-15 05:05发布

问题:

There are several scenarios where I need to invoke multiple asynchronous calls (from the same event handler) that can proceed independently of each other, with each one having its own continuation for updating the UI.

The following naive implementation causes the three asynchronous operations to be executed sequentially:

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

There's an MSDN example that suggests separating the creation of the tasks from their respective await statements, allowing them to be run in parallel:

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

However, the limitation of this approach is that the task continuations are still registered sequentially, meaning that the nth continuation can't execute before all its preceding n−1 continuations have completed, even though its task may be the first to complete.

What is the best pattern for getting the asynchronous tasks to run in parallel, but have each continuation run as soon as its respective task completes?

Edit: Most of the answers suggest awaiting on Task.WhenAll, like below:

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

However, this does not meet my requirement, as I need each continuation to execute as soon as its respective task completes. For example, suppose that GetNameAsync takes 5 s, GetCityAsync takes 2 s, and GetRankAsync takes 8 s. The implementation above would cause all three textboxes to only be updated after 8 s, even though the results for nameTextBox and cityTextBox were known much earlier.

回答1:

The traditional approach was to use ContinueWith for registering the respective continuation to each asynchronous task:

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

With C# 5, it is now preferable to use an await-style pattern. This may be most easily achieved by splitting up each task–continuation pair into its own method:

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

Defining all these trivial methods quickly become cumbersome, so one may condense them into async lambdas instead:

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

If this pattern is used frequently, it would also be helpful to define a utility method that can take the Func<Task> lambdas and execute them, making our event handler's code more concise and readable:

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


回答2:

A simpler alternative would be:

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

No closures, no extra state machines.



回答3:

As I understand it, you need to query three async resources and only update the UI once all three have returned. If that's correct the Task.WhenAll control is idea to use. Something like

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


回答4:

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

More details: https://msdn.microsoft.com/pt-br/library/dd270695(v=vs.110).aspx