Monday, April 18, 2016

Yet another short async/await example

The async/await pattern is here for some time and this is going to be yet another short example that could possibly make it easier for someone to grasp the idea.

First, since we talk about possible async operations, let’s start with a Windows.Forms/WPF application where there is at least a chance to see that an operation is async – we will start a long running operation and while it lasts, we the GUI will remain responsive. On the other hand, trying anything async from a console app doesn’t make much sense.

So let’s have a Windows.Forms app with two buttons. Assign something instant to the second button, something like MessageBox.Show so that you can press it and make sure the app remains responsive. We will then make the first button run a long operation.

Let’s start with this long running operation:

private int SlowOperation()
    Thread.Sleep( 5000 );    
    return 17;

It is obvious that running this from a click handler would freeze the application for 5 seconds.

Without async/await, you still can run this asynchronously. There are numerous ways to do it, a possibly most basic one is to create a delegate and run it asynchronously, this is something delegates support since .NET has been released:

Func<int> f = () => SlowOperation();
    ar =>    
        Func<int> s = (Func<int>)ar.AsyncState;        
        var result = s.EndInvoke( ar );        
        MessageBox.Show( result.ToString() );    
    f );

This looks ugly. Not only I need a pair of BeginInvoke/EndInvoke methods but also I need to feed both with valid arguments. In particular, if the delegate returns a value, a common practice is to pass the delegate as the “async state” so that it can be later picked up and used to end the async call. This basic pattern of Begin…/End… is called the Asynchronous Programming Model (APM).

However, this works. You can run the app and make sure it stays responsive and after 5 seconds it just shows a message box with the result of the long call.

As .NET evolved, this basic pattern has been evolving too. We had background workers, we had so called Event-Based Asynchronous Pattern (EAP) which is obsolete nowadays. A newest addition to the toolbox is the Task-based Asynchronous Pattern (TAP) which is based on the Task class that represents both initiation and completion of an async method.

And this is what this tutorial aims to – to show how this ugly code from above can be rewritten to a more modern style. We will refactor the code in three steps.

Step one. This is where we introduce the Task class. It also evolves but at the time I write this, you can pretty much easily convert anything to a task with Task.Run facade. It uses the thread pool to queue the async operation and immediately starts it.

The problem with bare Task.Run is that it returns a task and I still to supply a delegate to provide a code that runs when the task completes:

Task<int> task = Task.Run( () => SlowOperation() );
task.ContinueWith( t => MessageBox.Show( t.Result.ToString() ) );

This still doesn’t look good but definitely better than the APM version.

Btw. Stephen Cleary has a nice explanation on how and why the Task.Run should be used only to invoke async methods rather than inside their implementations.

Step two. In this step I rewrite my original SlowOperation to already support the async model. This way I won’t need the Task.Run anymore:

private Task<int> SlowOperation2()
    return Task.Delay( 5000 )        
               .ContinueWith( t => 17 );

Note, that I also had to change the blocking Thread.Sleep to a non blocking Task.Delay.

Now I go back to the client code and I rewrite it to

    .ContinueWith( t => MessageBox.Show( t.Result.ToString() ) );

Step three. This is where the async/await is finally introduced – it is merely a syntactic sugar over .ContinueWith.

First, I rewrite the SlowOperation2. Instead of the lambda that shows the continuation, the code is now clean.

private async Task<int> SlowOperation2()
    await Task.Delay( 5000 );    
    return 17;

Then I rewrite the client code to also replace the ContinueWith with async/await. This time it is not that easy because to be able to use await, my method has to be marked with async.

private async void button1_Click( object sender, EventArgs e )
    var result = await SlowOperation2();    
    MessageBox.Show( result.ToString() );

Happy asyncing/awaiting your code.

No comments: