29

I have an application that pulls a fair amount of data from different sources. A local database, a networked database, and a web query. Any of these can take a few seconds to complete. So, first I decided to run these in parallel:

Parallel.Invoke(
   () => dataX = loadX(),
   () => dataY = loadY(),
   () => dataZ = loadZ()
);

As expected, all three execute in parallel, but execution on the whole block doesn't come back until the last one is done.

Next, I decided to add a spinner or "busy indicator" to the application. I don't want to block the UI thread or the spinner won't spin. So these need to be ran in async mode. But if I run all three in an async mode, then they in affect happen "synchronously", just not in the same thread as the UI. I still want them to run in parallel.

spinner.IsBusy = true;

Parallel.Invoke(
     async () => dataX = await Task.Run(() => { return loadX(); }),
     async () => dataY = await Task.Run(() => { return loadY(); }),
     async () => dataZ = await Task.Run(() => { return loadZ(); })
);

spinner.isBusy = false;

Now, the Parallel.Invoke does not wait for the methods to finish and the spinner is instantly off. Worse, dataX/Y/Z are null and exceptions occur later.

What's the proper way here? Should I use a BackgroundWorker instead? I was hoping to make use of the .NET 4.5 features.

Amal K
  • 4,359
  • 2
  • 22
  • 44
Paul
  • 5,700
  • 5
  • 43
  • 67
  • Im wondering, you say "local database, network database, web query". Sounds like all three are IO based operations. Do any of them expose an asynchronous endpoint? – Yuval Itzchakov Jun 19 '14 at 18:14
  • No. Though they could. Currently they are simply database queries to a networked SQL Server, an Azure server, and a web POST to a separate server which returns other data. Each of these three run synchronously. – Paul Jun 19 '14 at 19:23
  • Well, an SQL Server query can be invoked with an async api via Entity Framework, i am sure Azure exposes some kind of async api, and a Post request can definitely be made using `HttpClient` via `PostAsync`. I dont think you really need to spin three threadpool threads for this – Yuval Itzchakov Jun 19 '14 at 19:29
  • 1
    Skeet's answer is the best for your situation. However, if your `dataX`…`dataZ` assignment targets happened to be UI-bound properties, then you could use the last code snippet I suggested in this answer: http://stackoverflow.com/a/23347895/1149773 – Douglas Jun 19 '14 at 19:45
  • @YuvalItzchakov No, using async for those three methods does not help. If one method called "await GetResponse()", then the UI thread is indeed free and the spinner moves. But the database query wouldn't get started until the web query was done. All three queries should fire at the same time, hence the use of parallel.Invoke originally. Skeet's answer solves this. – Paul Jun 19 '14 at 20:08
  • @Paul Thats incorrect. You can fire all three async tasks at the same time and then `await Task.WhenAll` on all of them, just without using parralel.invoke – Yuval Itzchakov Jun 19 '14 at 20:50
  • @YuvalItzchakov My mistake. I thought you wanted me to do something on the lines of StartSpinner(); TaskXAsync(); TaskYAsync(); TaskZAsync(); StopSpinner(); – Paul Jun 20 '14 at 13:32

2 Answers2

55

It sounds like you really want something like:

spinner.IsBusy = true;
try
{
    Task t1 = Task.Run(() => dataX = loadX());
    Task t2 = Task.Run(() => dataY = loadY());
    Task t3 = Task.Run(() => dataZ = loadZ());

    await Task.WhenAll(t1, t2, t3);
}
finally
{
    spinner.IsBusy = false;
}

That way you're asynchronously waiting for all the tasks to complete (Task.WhenAll returns a task which completes when all the other tasks complete), without blocking the UI thread... whereas Parallel.Invoke (and Parallel.ForEach etc) are blocking calls, and shouldn't be used in the UI thread.

(The reason that Parallel.Invoke wasn't blocking with your async lambdas is that it was just waiting until each Action returned... which was basically when it hit the start of the await. Normally you'd want to assign an async lambda to Func<Task> or similar, in the same way that you don't want to write async void methods usually.)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Perfect, thank you. An answer to a question in only 5 minutes. No wonder this site is my homepage. – Paul Jun 19 '14 at 12:39
  • When comparing this answer to Itzchakov's answer below, and using Ben Willi's tutorial (https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/) to demo this I find this answer runs in parallel, but Itzchakov's answer does not. – barrypicker May 10 '18 at 21:50
9

As you stated in your question, two of your methods query a database (one via sql, the other via azure) and the third triggers a POST request to a web service. All three of those methods are doing I/O bound work.

What happeneds when you invoke Parallel.Invoke is you basically trigger three ThreadPool threads to block and wait for I/O based operations to complete, which is pretty much a waste of resources, and will scale pretty badly if you ever need to.

Instead, you could use async apis which all three of them expose:

  1. SQL Server via Entity Framework 6 or ADO.NET
  2. Azure has async api's
  3. Web request via HttpClient.PostAsync

Lets assume the following methods:

LoadXAsync();
LoadYAsync();
LoadZAsync();

You can call them like this:

spinner.IsBusy = true;
try
{
    Task t1 = LoadXAsync();
    Task t2 = LoadYAsync();
    Task t3 = LoadZAsync();

    await Task.WhenAll(t1, t2, t3);
}
finally
{
    spinner.IsBusy = false;
}

This will have the same desired outcome. It wont freeze your UI, and it would let you save valuable resources.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • +1: Good point; this avoids blocking the thread-pool threads needlessly. – Douglas Jun 19 '14 at 21:30
  • I'm not using Parallel.Invoke anymore. How is this different than Jon's answer? Both execute three methods without blocking UI. The three methods in Jon's case are synchronous yes, but why is that a problem? Shouldn't the same resources get used for each? – Paul Jun 20 '14 at 13:36
  • When you use `Task.Run`, the `Task` class queues work on the `ThreadPool` and uses one of its threads to execute your work. When you consume an api that is truely async, there is no need for a thread. The method runs synchronously until reaching the `await` keyword on the async operation (might be an http request, db call, etc) and registers a continuation which will be invoked once the async operation completes. i suggest you read http://blog.stephencleary.com/2013/11/there-is-no-thread.html by stephan cleary which has a great explanation on whats going on behind the scenes. – Yuval Itzchakov Jun 20 '14 at 13:41
  • Incidentally, there are no async methods I can use here. The two database queries utilize Linq. So the bowels of the SQL queries are handled via deferred execution and get called when the "ToList()" method is called at the end of the IEnumberable query that gets modified via search parameters. – Paul Jun 20 '14 at 13:42
  • Yes, I get that. But like I said above - I'm not using Parallel.Invoke. Jon's answer doesn't include that. I'm starting three tasks and waiting on them. So is there a difference between starting three async tasks and three synchronous ones? – Paul Jun 20 '14 at 13:43
  • I edited my comment, the `Task` class and the `Parallel` class both utilize the ThreadPool. The difference between starting three sync method with `Task.Run` and using three async methods is that the async methods wont spin any new threads **at all** (though they will use IOCP threads in order to invoke the continuation), while the sync methods with `Task.Run` will use **three ThreadPool threads** – Yuval Itzchakov Jun 20 '14 at 13:46
  • 1
    If you're forming a LINQ query via `IQueryable`, you can use `QueryExtensions.ToListAsync`: http://msdn.microsoft.com/en-us/library/dn220262(v=vs.113).aspx – Yuval Itzchakov Jun 20 '14 at 13:48
  • 1
    Very cool, thank you for all the information! Will install the NuGet package and convert my code. – Paul Jun 20 '14 at 14:58
  • When comparing this answer to Skeet's answer and using Ben Willi's tutorial (https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/) to demo this I find this answer does not run in parallel, but Skeet's answer does. – barrypicker May 10 '18 at 21:48