Parallel vs Asynchronous
In the previous post we looked at how the Task Parallel Library can execute code at the same time. In this post we are going to look at asynchronicity.
A Story
First a true story. I was once at the Ikea and needed three extra pieces
that were not stored in the open warehouse. I needed to go to a counter
and wait for my turn. I asked the employee for my three pieces and she
sent three other employees to fetch the three pieces. She then asked me
to wait a little while she helped the next person in line.
By the time the three pieces were delivered to me, she had already helped
two other customers.
“What a beautiful analogy for parallelism and asynchronicity”, I thought by myself. “I must remember this for when I ever write a blog about the subject.”
Applied to asp.net
Just like there is only a limited number of Ikea counters, there is a limited number or resources in aps.net to handle http-requests: Worker Threads. If a request enters the asp.net pipeline, one of these threads is assigned to handle the request. In principle, this thread is only free for the next request when it has completely finished the job. We can do our work faster by parallelizing.
This is sending the three warehousemen to get my missing pieces. They parallelize the work so I don’t have to wait as long. But the interesting thing was that the person at the counter didn’t wait until the three pieces were delivered before attending to the next customer. I was still waiting, but there was no reason not to help the next customer.
To The Codes!
How to build this in aps.net? In MVC3 we got AsyncController. If we use that, we can use the following pattern in our action methods:
public class ParallelController : AsyncController
{
public void IndexAsync()
{
}
public ActionResult IndexCompleted(string one, string two, string three)
{
}
}
The IndexAsync
method is where the request enters. Here we will start our
asynchronous operations. IndexCompleted
is called when all operations are
completed. To coordinate all this, we need to use AsyncManager. First we
need to indicate how many operations will be executed before the
IndexCompleted
action can be called:
public void IndexAsync()
{
AsyncManager.OutstandingOperations.Increment(3);
}
Then we start our operations using the TPL. Each time an operation is finished
we need to signal the AsyncManager
. The result can be stored using the indexer on Parameters
. The name we give the index needs to be the same as
a parameter on the IndexCompleted
method.
public void IndexAsync()
{
AsyncManager.OutstandingOperations.Increment(3);
Task.Factory.StartNew(() => TestOutput.DoWork("1")
.ContinueWith(t =>
{
AsyncManager.OutstandingOperations.Decrement();
AsyncManager.Parameters["one"] = t.Result;
});
Task.Factory.StartNew(() => TestOutput.DoWork("2"))
.ContinueWith(t =>
{
AsyncManager.OutstandingOperations.Decrement();
AsyncManager.Parameters["two"] = t.Result;
});
Task.Factory.StartNew(() => TestOutput.DoWork("3"))
.ContinueWith(t =>
{
AsyncManager.OutstandingOperations.Decrement();
AsyncManager.Parameters["three"] = t.Result;
});
}
Now what happens under the cover is that when all tasks are started, the current Worker Thread is set free to handle other requests. Just like the Ikea employee can handle the next customer in the queue while I was waiting.
When the OutstandingOperations
of the AsyncManager
are decremented to
zero, the IndexCompleted
method is called on a new Thread that spun up
with the same (http)context of the original. This thread is used to handle
the rest of the request.
public ActionResult IndexCompleted(string one, string two, string three)
{
return View(new TestOutput{ One = one, Two = two, Three = three);
}
Just like our previous version, it takes two seconds to handle the request. The big difference is that the Thread is freed up to handle another request. This is asynchronicity and greatly increases the throughput of a system.
It’s a pity that the code is so ugly. All that plumbing with the
AsyncManager
, splitting your action in two parts, magic strings,…
C# 5 To The Rescue!
They must have thought the same in Redmond. That is why version 5 of C#
introduced two important new keywords: async
and await
.
The async
keyword signals that the function will contain asynchronous work.
When the compiler sees await
keyword it will asynchronously execute that
line while freeing up the current thread. When the line containing await
is
executed, a new thread is spun up to continue the rest of the method. Sounds
complicated, but the code will make it clearer. Our action can now be rewritten
as follows:
public async Task<ActionResult> Index()
{
var results = await Task.WhenAll(
Task.Run(() => TestOutput.DoWork("one")),
Task.Run(() => TestOutput.DoWork("two")),
Task.Run(() => TestOutput.DoWork("three"))
);
View(new TestOutput
{
One = results[0],
Two = results[1],
Three = results[2]
});
}
That’s much cleaner. We have the same functionality as before: the tasks are still executed in parallel and the Worker Thread is still set free for other work. But the code is readable again and not littered with plumbing.