다음을 통해 공유


What happens when you await? – Making it asynchronous (part II)

his mini-series of three blog entries dives into what actually happens when you use the await keyword in C#. We will evolve a small code-base from a synchronous to a scaled-out asynchronous implementation, and get into the details as we go along. This is the second post and it describes how we convert the code to run asynchronously and explains what really happens when you await.

The importance of asynchronous

In the previous post we detailed a small sample app that illustrated the severe problems with synchronous code. This is not a new problem of course. Keeping a user interface responsive while performing lengthy (and occasionally CPU intensive) work in the background is mandatory in most if not all apps today. Throughout the years, a lot of different techniques and patterns have emerged to deal with this issue. The plain truth about must of them has always been that they’re extremely hard to get right, and the kind of bugs you encounter are some of the hardest to chase down:

  • Deadlocks
  • Race conditions
  • Memory overwrites
  • Thread context switching

In WinRT (the application API for Windows Store apps) asynchronous functions plays a major part. In fact, if an API can take more than tens of milliseconds to complete, it only exists as an asynchronous function. So for example, the file I/O API has an asynchronous interface only. Now that we encounter more and more asynchronous parts in our code we need to have a pattern that lets us handle this complexity as easily as possible. We think async and await delivers on that promise and we will now explore how we can leverage those constructs.

IAsyncAction in WinRT

We talked about how much of WinRT’s API is asynchronous. The way it’s exposed is that every asynchronous function in WinRT returns a variant of this interface:

     public interface IAsyncInfo
    {
        AsyncStatus Status { get; }
        HResult ErrorCode { get; }
        uint Id { get; }
        void Cancel();
        void Close();
    }

This interface has 4 derived interfaces:

  • With or without return value
  • With or without a progress handler to give feedback on the progress of the operation

We will ignore the ones with progress for now, since they are not relevant for this discussion (and are very straight-forward if you understand the following two). That means we have two interfaces:

     public interface IAsyncAction : IAsyncInfo
    {
        AsyncActionCompletedHandler Completed { get; set; }
        void GetResults();
    }
    public interface IAsyncOperation<TResult> : IAsyncInfo
    {
        AsyncOperationCompletedHandler<TResult> Completed { get; set; }
        TResult GetResults();
    }

So, both interfaces has a Completed property where you assign a delegate to be called once the asynchronous operation has completed, and a function to get the results of the operation.

The IAsyncInfo* interfaces are a variant of futures, that is a contract that is a promise of a future result. Basically what all these asynchronous functions says when they give you back this interface is that they will promise you a result if the function is successful, or a failure if the function fails.

In WinRT, there is a static function on the ThreadPool class that can run an operation asynchronous. It looks like this:

     public static IAsyncAction RunAsync(WorkItemHandler handler);

This method allows us to easily rewrite our synchronous loop like this:

     private void RunScenario()
    {
        for (int i = 0; i < NumThreads; i++)
        {
            double pival = 0;
            var op = ThreadPool.RunAsync(delegate { pival = pi(); });
            op.Completed = delegate(IAsyncAction asyncAction, 
            AsyncStatus asyncStatus)
            {
                WorkDone(i, pival);
            };
        }
    }
   

This is in fact how you would write in both in Javascript and C++. In Javascript we will get back a promise, in C++ we could wrap the IAsyncInfo in a Parallel Patterns Library (or PPL) Task.

Task, async and await

In C#, the object that wraps asynchronous operations is called a Task. Task was introduced in .NET 4.0 and has been extended to support these scenarios as well. With Task, we can rewrite the code to look like this:

     private void RunScenario()
    {
        for (int i = 0; i < NumThreads; i++)
        {
            var task = Task.Run(() => { return pi(); });
            task.ContinueWith((op) => { WorkDone(i, op.Result); });
        }
    }
    

So this code will work (sort of) but it has a couple of limitations.

  • This code will actually run in parallel and that may or may not be what you want. If you don’t want parallel execution, the code gets a LOT more advanced
  • If you would have wanted to execute several asynchronous operations in sequence, the code becomes quickly convoluted

But above all, writing asynchronous code is hard and convoluted, it’s a lot easier to write synchronous code. But, what if you could write synchronous code that execute asynchronously? Wouldn’t that be good?

Turns out you can.

C# and .NET has two new keywords enable this, the async and await keywords. With them we can rewrite the function above to something like this:

     private async Task RunScenario()
    {
        for (int i = 0; i < NumThreads; i++)
        {
            var pival = await Task.Run(() => { return pi(); });
            WorkDone(i, pival);
        }
    }
    

First, attributing a function with the async keyword tells the C# compiler that this function will make asynchronous calls. This will cause the compiler to rewrite this function as a state machine to handle this. I will not go into details about this, see Stephen Toub’s more than excellent blog post about this. The net effect of this is that we can use await to call asynchronous functions in a synchronous matter.

If we dissect the code, what will actually happen is that the function starts to execute on the UI thread, but as soon as the asynchronous call returns, the function returns as well and the UI thread continues to execute other code. The actual work done by the asynchronous code executes on a background thread and when the asynchronous call finally completes, the code is marshaled back to the UI thread and execution continues on the line below the await call.

So in effect what we have is that we can write the code synchronously and it will execute asynchronously through magic performed by the C# compiler.

Testing the code

Running the code we get a result like this:

image

Looking at it while running it looks a lot better. The user interface is responsive, the rectangles gets green one-by-one. The only thing that doesn’t work is the Clock. Why did it stop after 23 milliseconds? It sure took longer than that to complete the operation? The answer of course, is that RunScenario now is an asynchronous function. Consider following function:

     private void Start_Click(object sender, RoutedEventArgs e)
    {
        SetupRun();
        RunScenario();
        FinishRun();
    }

Now, SetupRun() creates the timer and FinishRun() stops the timer. Since RunScenario() is asynchronous now, it will return when it encounters the first await, making the code go on and call FinishRun() prematurely. The easy and elegant solution is to use await to wait for the RunScenario() function to complete. This requires us to mark Start_Click() with the async keyword as well. The code will then look like this:

     private async void Start_Click(object sender, RoutedEventArgs e)
    {
        SetupRun();
        await RunScenario();
        FinishRun();
    }

Running the code again proves that we we right. Now the Clock runs during processing and displays the accurate end time. After the run, it looks like this:

image

Summary

In this blog post, we’ve shown you why asynchronous code is important, How asynchronous functions are implemented, how async and await really makes this a lot easier and how little impact that pattern makes on your code. Are we done? No, we still run the computation in sequence and we don’t take advantage of multiple cores in our CPU, There’s a lot more potential to uncover, and a lot of interesting side-effects to handle. Read this post.