Fanning Out Async Await
I wanted to run an async function across a number of items, and I wanted to do this in parallel. This turned out to be quite a straightforward thing to do, once you know how.
Let’s find out how, by trying out a few different approaches.
First of all, here is a piece of async work that needs to be done 100 times. The total needs to be summed up:
Failed Attempt #1
Now, let’s take a really naive approach to running this:
This takes 11000ms to run.
Although we’re using the funky async/await keywords here, we’re running the code in series, rather than parallel. The ‘await’ keyword will block the execution of the loop until ‘DoWork’ returns.
Failed Attempt #2
Let’s try to improve on this. Where is the first place to go for parallel execution in .NET, the Task Parallel Library right?
This takes 65 ms to run.
Looking good, we’re doing a Parallel For loop now, and we’re using an async lambda. Cool stuff. It also takes just 65ms to run, wow, that’s shorter than the 100ms delay on the task! Unfortunately the TPL doesn’t work well with async/await. The For loop returns before the async tasks have completed. We never get the answer.
Failed Attempt #3
Ok, let’s have another go, and use the Task.WhenAll function, which returns a task which completes when all the tasks you pass it have completed:
This takes 45ms to run.
Even though we’re using the Task.WhenAll, this doesn’t work either. Task.Start doesn’t seem to be compatible with ‘WhenAll’ when async tasks are used. At least it’s faster than the last approach that didn’t work.
Failed Attempt #4
There is a Task.Run, which should be used in this scenario. Let’s modify the sample and try again:
This takes 180ms to run.
Instead of starting the task, we use Task.Run to create and start the task. This now seems to work, and we’re in business. So what’s wrong? Well it turns out that Task.Run dispatches the code to run on the thread pool rathern than the current thread. This isn’t what I want. There’s also a lot of code here, and it doesn’t look very elegant.
Back to the drawing board, and this is what I came up with:
This takes 160ms to run.
When you see the answer it seems obvious, you don’t use await in the loop, you just add all the tasks (promises) to a list, and await the lot when you’ve finished creating them.
Async/await makes code look clean, but the async/parallel/task story in .NET is not a clear one. The parts don’t work together like you’d expect.
You’re never quite sure where your task is going to run, on the thread pool or on the current thread?
Thorough testing of async code is necessary. When awaits are short the await command will return immediately, which means that in real life the scenarios that didn’t work (i.e. attempts #2 & #3) will work correctly. This leads to some weird bugs and race conditions.