3

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.

Douglas
  • 53,759
  • 13
  • 140
  • 188

4 Answers4

3

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());
}
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • 1
    I would upvote this if you moved `ContinueWith` as a *last* option or removed it entirely. `await` is the new `ContinueWith`. – Stephen Cleary Apr 28 '14 at 18:47
  • @StephenCleary: I agree that `ContinueWith` is the least preferred option. I placed it first as it's the easiest to digest by people who are still getting accustomed to the semantics of `await` (and sets the context for my final recommended answer). Btw, I wanted to say thanks and well done for your articles on `async` and `await` (especially [Best Practices in Asynchronous Programming](http://msdn.microsoft.com/en-us/magazine/jj991977.aspx)); I learnt about the new features mostly through them. – Douglas Apr 28 '14 at 19:04
  • 1
    I prefer to define each "forking" task lambda individually as a "nested" function, like [this](http://stackoverflow.com/a/23216300/1768303). – noseratio Apr 28 '14 at 22:26
3

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.

Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • This is a neater implementation of focuspark's suggestion; however, it suffers from the same limitation – that the textboxes are only updated after the slowest task completes. – Douglas Apr 29 '14 at 18:09
  • Indeed. For updating as soon as data is available your second suggestion would be the best choice. – Paulo Morgado Apr 29 '14 at 22:00
1

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;
}
whoisj
  • 398
  • 1
  • 2
  • 10
  • Thanks! In my scenario, I needed to update the UI as soon as each task completes. Your solution is also valid for the scenario you described, although I would rewrite it as: `var nameTask = GetNameAsync();` … `await Task.WhenAll(nameTask, …);` `nameTextBox.Text = nameTask.Result;` … – Douglas Apr 28 '14 at 18:38
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;
}

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