Task-Based Asynchronous Pattern in .NET 4.5 – Part 1

In a previous article we saw an overview of the new IDE and Framework features of the current .NET Framework beta v4.5. Among other things .NET 4.5 has an improved support for Asynchronous programming through a new Task based model. In this article, we will take a look at the new async and await key words introduced in the C# language.

Asynchronous programming has always been considered a niche and has always been delegated as a ‘good-to-have’ in v2 of something that you were working with. Moreover lack of Async APIs meant you had to wrap async functionality first before you could use them in your code. Basically a lot of plumbing was potentially required to get off the ground with Async programming.

So the synchronous API for reading into a byte buffer would traditionally look as follows:

public class MyReader
  
{

  
    public int Read(byte [] buffer, int offset, int count);

  
}

The pre-TPL API would look something like the following and you would have to setup the callback method yourself and track the End[Operation].

public class MyReader
  
{

  
    public IAsyncResult BeginRead(

  
        byte [] buffer, int offset, int count, 

  
        AsyncCallback callback, object state);

  
    public int EndRead(IAsyncResult asyncResult);

  
}

Instead of the call-back model, one could use the Event based model and write it up as follows

public class MyReader
  
{

  
    public void ReadAsync(byte [] buffer, int offset, int count);

  
    public event ReadCompletedEventHandler ReadCompleted;

  
}

public delegate void ReadCompletedEventHandler(
  
    object sender, ReadCompletedEventArgs eventArgs);

public class ReadCompletedEventArgs : AsyncCompletedEventArgs
  
{

  
    public int Result { get; }

  
}

But with the introduction of Task Parallel Library (TPL), asynchronous programming became easier. To make the above, we would simply need the following code

public class MyReader
  
{

  
    public Task<int> ReadAsync(byte [] buffer, int offset, int count);

  
}

  
Now the above method signature shows how an Async method would be implemented. It returns a Task<T> where T is of type required for the return and the method itself by convention has the word Async appended to it.
Apart from the lesser plumbing required, the framework added a huge collection of Async methods by default. So going forward in this article, we will look at how to consume Async methods provided in the framework.

Consuming an Async call and the ‘async’/‘await’ keywords

As mentioned above the latest .NET framework provides a lot of Async counterparts of older Synchronous method calls. Specifically anything that could potentially take more than 100ms now has an Async counterpart. So how do we use these async methods?
In .NET 4.5 we use the async and await keywords while consuming Asynchronous APIs. So to consume the above ReadAsync we would write code as follows

get-data-async

The method signature when marked with async keyword tell the compiler of our intent to use Async method and that we wish to ‘await’ returning from these Async methods. So the compiler expects one more await keywords in the method. If none are provided, compiler will generate a warning.
Under the hood, the compiler generates a delegate for code after the await key word is used and moves execution to the end of the method such that the method returns immediately. Thereafter the delegate is called once the async method is complete.

Things to look out for

All looks pretty neat, where is the catch?
Well there are quite a few when leveraging the Async framework.
Synchronization Context
Though thread synchronization has now been hidden away from you, does not mean it’s not happening. For example an async read operation that is initiated from the UI thread will have a SynchronizationContext stashed away and every thread completion will result in a hop back to the initiation thread. This is a good idea most of the time because for end users the hop back means ability to show progress on the UI or work on the UI thread. However if this call is initiated from UI thread and passed on to a library whose sole purpose it to read the data asynchronously, the overhead for hopping back and forth between the execution thread and the UI thread is very high and big performance killer. To avoid this Sync context can be skipped using the following syntax

…
  
{

  
    …

  
    int bytesRead = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false));

  
    …

  
}

What ConfigureAwait(false) does is, it tells the framework not to hop back into the initialization thread and continue if possible.
Avoiding Deadlocks
Incorrect use of the ‘Wait’ call instead of the synchronous ‘Await’ call may cause deadlocks because issuing a Wait on the initialization thread blocks the UI thread, and when ReadAsync tries to synchronize it can’t so it waits resulting in an unending wait. This situation can be avoided if we configure TPL to avoid synchronization context using the ConfigureAwait(false).
How Garbage Collection is affected
Use of the async keywords results in creation of an on-the-fly state machine structs that stashes away the local variables. Thus these local variables are ‘lifted’ up from the ‘stack’ allocation and moved into the heap. Hence even through they look like local variables they definitely stay around for longer.
Moreover .NET’s GC is generational and sometimes multiple sync points might push the allocated ‘locals’ into an older generation thus keep them around even longer before they are eventually collected.
Asynchronicity and Performance
Asynchronous operations are more about scaling and responsiveness rather than raw performance. Almost all asynchronous operations suffer a performance for a single operation due to the overheads introduced. Hence do not treat Performance as the criteria for doing async operations. True performance gain is an aggregate of how better your async implementation uses resource better.
Client vs. Server Async operations
Just as performance is not the reason to do Async, it may not be a good fit for server side operations either. On the client side, asynchrony is all about getting the UI thread freed up for user-interaction as soon as possible by pushing compute intensive tasks off it. But on Server side, operations deal with processing a request and responding to it as quickly as possible. There is no UI thread per se. Every thread is a worker thread and any thread blocked is an operation in wait. For example in ASP.NET the thread pool is revered resource. Unnecessary thread spinning by indiscriminately using threads increase CPU resource requirements and reduce server scaling. Remember in case of servers the most important thing to do is to avoid context switches. Choose an async operation wisely for example when you have I/O intensive operations, using async with correctly configured awaits will actually help the operation.

Going forward

This was a quick introduction to the basic features of the Task-Based Asynchronous Pattern in the upcoming framework release. We will explore more features of this implementation in a follow up article.


Will you give this article a +1 ? Thanks in advance


1 comment:

SerialSuccess said...

Very interesting article and this is, perhaps, the solution I have been scrambling to find. I was hoping for some insights if possible. I currently have an external C++ DLL that takes between 5 and 30 seconds to process a given set of data I send to it. The main delay in the DLL are calls to the Internet which take 99% of the time to complete. All of that work - the calls and processing - are encapsulated from my .NET program. I literally have millions of records to process and, even though I am using TPL, it is still incredibly slow. To that end, I was hoping to use ASYNC calls to this DLL to make things run faster. Sort of a fire-and-forget on the frontend with something to catch the responses when they come back. That said, the C++ DLL needs to run in process. Will this approach you describe work for me and, if not, do you know of any other options in 4.5?