C# 5 async: investigating control flow

Yes, this has been a busy few days for blog posts. One of the comments on my previous blog post suggested there may be some confusion over how the control flow works with async. It’s always very possible that I’m the one who’s confused, so I thought I’d investigate.

This time I’m not even pretending to come up with a realistic example. As with my previous code, I’m avoiding creating a .NET Task myself, and sticking with my own custom "awaiter" classes. Again, the aim is to give more insight into what the C# compiler is doing for us. I figure other blogs are likely to concentrate on useful patterns and samples – I’m generally better at explaining what’s going on under the hood from a language point of view. That’s easier to achieve when almost everything is laid out in the open, instead of using the built-in Task-related classes.

That’s not to say that Task doesn’t make an appearance at all, however – because the compiler generates one for us. Note in the code below how the return type of DemonstrateControlFlow is Task<int>, whereas the return value is only an int. The compiler uses Task<T> to wrap up the asynchronous operation. As I mentioned before, that’s the one thing the compiler does which actually requires knowledge of the framework.

The aim of the code is purely to demonstrate how control flows. I have a single async method which executes 4 other "possibly asynchronous" operations:

  • The first operation completes synchronously
  • The second operation completes asynchronously, starting a new thread
  • The third operation completes synchronously
  • The fourth operation completes asynchronously
  • The result is then "returned"

At various points in the code I log where we’ve got to and on what thread. In order to execute a "possibly asynchronous" operation, I’m simply calling a method and passing in a string. If the string is null, the operation completes syncrhonously. If the string is non-null, it’s used as the name of a new thread. The BeginAwait method creates the new thread, and returns true to indicate that the operation is completing asynchronously. The new thread waits half a second (to make things clearer) and then executes the continuation passed to BeginAwait. If you remember, that continuation represents "the rest of the method" – the work to do after the asynchronous operation has completed.

Without further ado, here’s the complete code. As is almost always the case with my samples, it’s a console app:

using System;
using System.Threading;
using System.Threading.Tasks;

public class ControlFlow
{
    static void Main()
    {
        Thread.CurrentThread.Name = "Main";
        
        Task<int> task = DemonstrateControlFlow();
        LogThread("Main thread after calling DemonstrateControlFlow");
        
        // Waits for task to complete, then retrieves the result
        int result = task.Result;        
        LogThread("Final result: " + result);
    }
    
    static void LogThread(string message)
    {
        Console.WriteLine("Thread: {0}  Message: {1}",
                          Thread.CurrentThread.Name, message);
    }
    
    static async Task<int> DemonstrateControlFlow()
    {
        LogThread("Start of method");
        
        // Returns synchronously (still on main thread)
        int x = await MaybeReturnAsync(null);        
        LogThread("After first await (synchronous)");
        
        // Returns asynchronously (return task to caller, new
        // thread is started by BeginAwait, and continuation
        // runs on that new thread).
        x += await MaybeReturnAsync("T1");        
        LogThread("After second await (asynchronous)");
        
        // Returns synchronously – so we’re still running on
        // the first extra thread
        x += await MaybeReturnAsync(null);
        LogThread("After third await (synchronous)");
            
        // Returns asynchronously – starts up another new
        // thread, leaving the first extra thread to terminate,
        // and executing the continuation on the second extra thread.
        x += await MaybeReturnAsync("T2");
        LogThread("After fourth await (asynchronous)");
        
        // Sets the result of the task which was returned ages ago;
        // when this occurs, the main thread
        return 5;
    }
    
    /// <summary>
    /// Returns a ResultFetcher which can have GetAwaiter called on it.
    /// If threadName is null, the awaiter will complete synchronously
    /// with a return value of 1. If threadName is not null, the
    /// awaiter will complete asynchronously, starting a new thread
    /// with the given thread name for the continuation. When EndAwait
    /// is called on such an asynchronous waiter, the result will be 2.
    /// </summary>
    static ResultFetcher<int> MaybeReturnAsync(string threadName)
    {
        return new ResultFetcher<int>(threadName, threadName == null ? 1 : 2);
    }
}

/// <summary>
/// Class returned by MaybeReturnAsync; only exists so that the compiler
/// can include a call to GetAwaiter, which returns an Awaiter[T].
/// </summary>
class ResultFetcher<T>
{
    private readonly string threadName;
    private readonly T result;
    
    internal ResultFetcher(string threadName, T result)
    {
        this.threadName = threadName;
        this.result = result;
    }
    
    internal Awaiter<T> GetAwaiter()
    {
        return new Awaiter<T>(threadName, result);
    }
}

/// <summary>
/// Awaiter which actually starts a new thread (or not, depending on its
/// constructor arguments) and supplies the result in EndAwait.
/// </summary>
class Awaiter<T>
{
    private readonly string threadName;
    private readonly T result;
    
    internal Awaiter(string threadName, T result)
    {
        this.threadName = threadName;
        this.result = result;
    }
    
    internal bool BeginAwait(Action continuation)
    {
        // If we haven’t been given the name of a new thread, just complete
        // synchronously.
        if (threadName == null)
        {
            return false;
        }

        // Start a new thread which waits for half a second before executing
        // the supplied continuation.
        Thread thread = new Thread(() =>
        {
            Thread.Sleep(500);
            continuation();
        });
        thread.Name = threadName;
        thread.Start();
        return true;
    }

    /// <summary>
    /// This is called by the async method to retrieve the result of the operation,
    /// whether or not it actually completed synchronously.
    /// </summary>
    internal T EndAwait()
    {
        return result;
    }
}

And here’s the result:

Thread: Main  Message: Start of method
Thread: Main  Message: After first await (synchronous)
Thread: Main  Message: Main thread after calling DemonstrateControlFlow
Thread: T1  Message: After second await (asynchronous)
Thread: T1  Message: After third await (synchronous)
Thread: T2  Message: After fourth await (asynchronous)
Thread: Main  Message: Final result: 5

A few things to note:

  • I’ve used two separate classes for the asynchronous operation: the one returned by the MaybeReturnAsync method (ResultFetcher<T>),  and the Awaiter<T> class returned by ResultFetcher<T>.GetAwaiter(). In the previous blog post I used the same class for both aspects, and GetAwaiter() returned this. It’s not entirely clear to me under what situations a separate awaiter class is desirable. It feels like it should mirror the IEnumerable<T>/IEnumerator<T> reasoning for iterators, but I haven’t thought through the details of that just yet.
  • If a "possibly asynchronous" operation actually completes synchronously, it’s almost as if we didn’t use "await" at all. Note how the "After first await" is logged on the Main thread, and "After third await" is executed on T1 (the same thread as the "After second await" message). I believe there could be some interesting differences if BeginAwait throws an exception, but I’ll investigate that in another post.
  • When the first "properly asynchronous" operation executes, that’s when control is returned to the main thread. It doesn’t have the result yet of course, but it has a task which it can use to find out the result when it’s ready – as well as checking the status and so on.
  • The compiler hasn’t created any threads for us – the only extra threads were created explicitly when we began an asynchronous operation. One possible difference between this code and a real implementation is that MaybeReturnAsync doesn’t actually start the operation itself at all. It creates something which is able to start the operation, but waits until the BeginAwait call before it starts the thread. This made our example easier to write, because it meant we could wait until we knew what the continuation would be before we started the thread.
  • The return statement in our async method basically sets the result in the task. At that point in this particular example, our main thread is blocking, waiting for the result – so it becomes unblocked as soon as the task has completed.

If you’ve been following along with Eric, Anders and Mads, I suspect that none of this is a surprise to you, other than possibly the details of the methods called by the compiler (which are described clearly in the specification). I believe it’s worth working through a simple-but-useless example like this just to convince you that you know what’s going on. If the above isn’t clear – either in terms of what’s going on or why, I’d be happy to draw out a diagram with what’s happening on each thread. As I’m rubbish at drawing, I’ll only do that when someone asks for it.

Next topic: well, what would you like it to be? I know I’ll want to investigate exception handling a bit soon, but if there’s anything else you think I should tackle first, let me know in the comments.

20 thoughts on “C# 5 async: investigating control flow”

  1. Are you doing this from your laptop with a half-installed CTP? That would be a different explanation for why you’re avoiding the .net Task type :)

  2. @configurator: No, this is on my “bigger” laptop which has the full CTP installed. Still writing it all in a text editor rather than Visual Studio though :)

  3. I’d really like to see when an Exception is thrown with void-returning async methods. Given an example like this (assume WaitHalfASecAsync is something like your example in this post):

    async void Exceptional() {
    await WaitHalfASecASync();
    throw new Exception(“Boom!”);
    }

    public static void Main(string[] args) {
    Console.WriteLine(“Calling exceptional”);
    Exceptional();
    Console.WriteLine(“Waiting for it to fail”);
    Thread.Sleep(1000);
    Console.WriteLine(“This shouldn’t happen!”);
    }

    Note that in my example I’m explicitly *not* awaiting the result of Exceptional(). If it’s, for example, a logger that sends an email, I never want to wait for the email to be sent. If it’s an archive function, I may never need to wait for the archive to finish (assuming this is done in a UI-less fashion so no feedback is needed).

  4. @configurator: I’ve just investigated this. Because Exceptional() returns void, the exception will be thrown in the new thread (and the CLR will terminate, IIRC). If you returned Task instead, the exception would be ignored.

    But yes, I’m happy to go into exception handling in the next post if you like. Heck, that can probably be tomorrow… although if anyone wants me to just shut up for a while, I’d be happy to comply with that too :)

  5. The term bigger laptop makes me think of the ThinkPad W701 4323. I’m thinking of buying one, if, you know, I win £5,357.59 in the lottery or something :)

  6. I also started investigating control flow with “await”, but in the context of interleaving method execution on the same thread: http://code.logos.com/blog/2010/10/coroutines_with_c_5s_await.html

    The subject of exception handling is interesting to me, particularly with regard to unhandled exceptions. (TPL’s default behaviour for unobserved exceptions is (unquestionably, in my view) the right thing to do, but wreaks havoc with post-mortem debugging.)

  7. I really shouldn’t have included the “_never_” in yesterday’s comment, as it isn’t literally true. I tried to clarify the point I was making over on Eric’s blog:

    “Async methods are meant to be composed.

    Just as the async method you are writing uses “await” to call various async methods in it’s body, the caller will be using ‘await’ to call you. This is the primary benefit, letting the whole call tree be structured in a simple, easy-to-reason-about fashion.

    Only at the very bottom (where asyncs are being built out of platform primitives), and at the very top (where an entire async workflow is being kicked off with a TaskEx.Run or such) will this pattern not hold true.”

    This issue may be at the heart of why it’s difficult to choose an optimal keyword. It depends on whether you are approaching it from how it is implemented or how it is intended to be used.

    My assertion remains that keyword naming should be based on programmer intention, not the implementation. (And hence calling the keyword “return ” would be very bad.)

  8. I’d be curious to know what (if any) special behavior there is for continuing execution on a UI thread. For example, depending on how MaybeAsync runs (sync/async), and whether the framework uses the current synchronization context to marshal the continuation, the following code may not run consistently:

    TextBox tb = …; // e.g. from WPF UI
    string s = await MaybeAsync();
    tb.Text = s; // returned to UI thread?

    Maybe this question is too simple, but so far I’ve only been keeping up with you and Eric, and have not had time to watch any videos or get the CTP to check for myself :)

  9. I get your point about ‘yield’ being potentially misleading, because control is not necessarily yielded. But I agree with an earlier commenter’s point that ‘yield’ does a good job at expressing the intent of the programmer. The programmer is saying, “I’m willing to YIELD control to the caller UNTIL the following method completes or causes an exception.” By the way, this English description of the programmer’s intent is also why I like ‘until’. ‘Yield until’ is a nice abbreviated way to capture the total sentiment. It’d be even better if I could think of a way to express the same concept in one word, but I’m not sure English has such a word.

    I have to admit I’m not a big fan of ‘continue after’. In trying to use these words to characterize the programmer’s intent, I end up with something less elegant and more vague: “I’m willing to give up control and CONTINUE execution of this method AFTER the following method completes or causes an exception.” Note that the notion of “giving up control” is implicit in the ‘continue after’ construction, whereas it is quite explicit in the ‘yield until’ formulation.

    Jon can probably do a better job than I did at paraphrasing the programmer’s intent using ‘continue’ and ‘after’.

  10. @Blake: The caller *may* be using await, or they may not. That doesn’t affect whether or not control returns to that caller. Control returns to the caller, which in turn gets to decide whether to wait for you to complete or not.

    @Empreror XLII: It’s up to the asynchronous operation to decide where the continuation will be run. It looks like the normal pattern will be to run the continuation via the caller’s current SynchronizationContext – which for UI threads will mean coming back to the UI thread, but for thread pool threads will mean firing the continuation on any thread pool thread (not necessarily the original one).

  11. @skeet: True, the caller isn’t required to use await, anymore than the caller of a method implemented with ‘yield’ is required to enumerate over the results. In both cases, however, that is how the feature is intended to be used and how it facilitates reasoning about the overall code.

    I’m not arguing about your analysis of the implementation internals, which is certainly correct. This was about why such a keyword should never be called “return until”.

  12. @Blake: I’d say it shouldn’t be called “return until” because it *sometimes* won’t return to the caller.

    It seems we look at the feature in very different ways. Even if the caller *is* using await, they still get control back – which may well mean that *they* then return control to *their* caller.

  13. I believe you are primarily focused on how the feature is implemented. That’s certainly very useful and important. The implementation is _completely_ different than the F# implementation that I’m more familiar with, by the way.

    My comments have been based on how the feature is intended to be used. In particular, I’m basing that on how the F# feature is currently being used, because while the implementations are night and day different, the surface interface is very similar. Anders was clear during the PDC Q&A that the C# feature is ‘heavily inspired’ by the F# one.

    Regarding that returning back to the caller’s caller and so forth – yes, and in fact it generally ends up returning back to the message loop/threadpool/TPL, which dispatches the correct continuation and maintains the lovely appearance of a simple, synchronous call tree, even though it is async under the covers. Therein lies the beauty.

  14. @Blake: I’m *currently* focused on how the feature is implemented, in order to create a solid personal understanding on top of which I can build everything else. That’s the way I like to work – I don’t like black magic, basically :) Things like LINQ made a lot more sense to me when I understood what the compiler was doing under the hood.

    On the other hand, even from the higher level, I can’t see how it makes sense to claim control isn’t returned to the caller. Note that even within an async method, the task may well not be *immediately* awaited – for example if you want to launch multiple tasks in parallel, which I suspect will be pretty common.

    I think it makes more sense as a mental model to think that control will usually be returned to the caller, but that the caller may choose to await the result. I think that puts the responsibility in the right place.

  15. I’ve had one of those ‘aha’ moments since my last comment. I believe a big part of my disconnect here is the difference between the ‘hot’ Task/Task’s that C# uses and the ‘cold’ Async‘s that F# uses.

    An Async is just a promise/future/etc, nothing runs until you run it. This is why the F# implementation doesn’t have the equivalent of BeginAwait returning false.

    The C# implemention is akin to every call to an F# async method being followed by “|> Async.StartImmediate”. That feels very unnatural, having been using the F# model for a while. I’m hoping there will be an Eric post dicussing why the C# team went a different direction in this area.

  16. @Blake: Yes, I believe that’s a definite difference… although one certainly *could* write a ColdTask which only started when GetAwaiter().BeginAwait(…) was called. That might be an interesting thing to experiment with, actually :)

  17. That leads straight to your next blog entry.

    An obvious advantage of cold tasks is when you want to compose them. Because they aren’t running yet, you can pass them to a piece of library code that manages their dependancies.

  18. @Blake: Right. And of course, one interesting feature is that you can easily build a hot task from a cold task via wrapping (by starting the cold task on construction), but not vice versa.

  19. Having just returned from Eric’s last posting, it appears that there won’t be any extensibility hooks to return your own ColdTask. Humbug.

    Assuming hot tasks remain the model in C# I suspect that wrapping his how all F# code will have to interop.

Comments are closed.