1

I have code that loads data from multiple sources using async methods (shown below).

public class ThEnvironment
{
    public async Task LoadLookupsAsync(CancellationToken cancellationToken) { ... }
    public async Task LoadFoldersAsync(CancellationToken cancellationToken) { ... }
}

Elsewhere in my program, I want to load multiple environments.

ThEnvironment[] workingEnvironments = ThEnvironment.Manager.GetWorkingEnvironments();
foreach ( ThEnvironment environment in workingEnvironments )
{
    await environment.LoadLookupsAsync(CancellationToken.None);
    await environment.LoadFoldersAsync(CancellationToken.None);
}

My question is twofold:

  1. For any single environment, how would I run both environment.Load...Async() methods in parallel and still be able to pass the CancellationToken?
  2. How would I do this for all environments in parallel?

Bottom line: I'd like to run all Load..Async() methods for all environments in parallel and still be able to pass the CancellationToken.


update

Thanks to everyone for the very quick answers. I've reduced the code down to the following:

Refresh();

var tasks = ThEnvironment.Manager.GetWorkingEnvironments()
    .SelectMany(e => new Task[] { e.LoadLookupsAsync(CancellationToken.None), e.LoadFoldersAsync(CancellationToken.None) });

await Task.WhenAll(tasks.ToArray());

I'm not too concerned about capturing all exceptions since this is not a production application. And the Refresh() at the top took care of the form's initial painting issue.

Great stuff all around.

2 Answers2

2

You can simply start both Tasks and then wait for both of them to complete:

Task lookupsTask = environment.LoadLookupsAsync(cancellationToken);
Task foldersTask = environment.LoadFoldersAsync(cancellationToken);

await lookupsTask;
await foldersTask;

A more efficient version of the same code would be to use Task.WhenAll():

await Task.WhenAll(lookupsTask, foldersTask);

If you want to do this for a collection of Tasks, not just two of them, you can fill a List<Task> with all the Tasks and then await Task.WhenAll(tasksList).

One issue with this approach is that if more than one exception happens, you're only going to get the first one. If that's a problem for you, you can use something like what 280Z28 suggested.

svick
  • 236,525
  • 50
  • 385
  • 514
1

Start both actions with the same cancellation token, and then wait for both of them to complete.

Task lookupsTask = environment.LoadLookupsAsync(cancellationToken);
Task foldersTask = environment.LoadFoldersAsync(cancellationToken);

await Task.Factory.ContinueWhenAll(
    new[] { lookupsTask, foldersTask },
    TaskExtrasExtensions.PropagateExceptions,
    TaskContinuationOptions.ExecuteSynchronously);

This uses the PropagateExceptions extension method from the Parallel Extensions Extras sample code to ensure that exception information from the load tasks is not lost:

/// <summary>Propagates any exceptions that occurred on the specified tasks.</summary>
/// <param name="tasks">The Task instances whose exceptions are to be propagated.</param>
public static void PropagateExceptions(this Task [] tasks)
{
    if (tasks == null) throw new ArgumentNullException("tasks");
    if (tasks.Any(t => t == null)) throw new ArgumentException("tasks");
    if (tasks.Any(t => !t.IsCompleted)) throw new InvalidOperationException("A task has not completed.");
    Task.WaitAll(tasks);
}
Sam Harwell
  • 97,721
  • 20
  • 209
  • 280
  • Your suggestion works well, but it appears to block the UI thread while executing those tasks. I'm calling the code in my main form's Show() event handler. My previous code allows the form to fully paint itself and locks down the controls prior to executing the tasks. Your suggested code does not fully paint the form until the execution of the tasks has completed. Is there a way to allow the UI thread to continue? (and thank your for your quick answer btw) – CodeWarrior Nov 08 '13 at 17:00
  • @CodeWarrior This code won't block the UI thread. Are you sure you're using it exactly as suggested? – svick Nov 08 '13 at 17:08
  • @svick, I may be mistaken about it actually blocking the UI thread. I am using it exactly as suggested. It does allow the form to load a bit faster, but the form does not fully paint itself during the first Show() event. That's not a huge deal though. Is the reason for using ContinueWhenAll() solely to propagate exceptions correctly? I tried a variant of the code using await Task.WhenAll() that appears to work the same way. – CodeWarrior Nov 08 '13 at 17:15
  • @CodeWarrior Yes, tracing exceptions in asynchronous code can be a nightmare, so I'm very careful to preserve the original exception along with the asynchronous code flow. The `WhenAll` method will not work that way. – Sam Harwell Nov 08 '13 at 18:48
  • @280Z28, thanks for showing `PropagateExceptions`, I've learnt a new trick. I'd like to have that option, although I'm not sure it's a big concern, IMO ([here's some discussion on that](http://stackoverflow.com/q/18314961/1768303)). – noseratio Nov 08 '13 at 22:11