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");
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");
int x = await MaybeReturnAsync(null);
LogThread("After first await (synchronous)");
x += await MaybeReturnAsync("T1");
LogThread("After second await (asynchronous)");
x += await MaybeReturnAsync(null);
LogThread("After third await (synchronous)");
x += await MaybeReturnAsync("T2");
LogThread("After fourth await (asynchronous)");
return 5;
}
static ResultFetcher<int> MaybeReturnAsync(string threadName)
{
return new ResultFetcher<int>(threadName, threadName == null ? 1 : 2);
}
}
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);
}
}
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 (threadName == null)
{
return false;
}
Thread thread = new Thread(() =>
{
Thread.Sleep(500);
continuation();
});
thread.Name = threadName;
thread.Start();
return true;
}
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.