I've been following the new announcement regarding the new async
feature that will be in c# 5.0. I have a basic understanding of continuation passing style and of the transformation the new c# compiler makes to code like this snippet from Eric Lippert's post:
async void ArchiveDocuments(List<Url> urls)
{
Task archive = null;
for(int i = 0; i < urls.Count; ++i)
{
var document = await FetchAsync(urls[i]);
if (archive != null)
await archive;
archive = ArchiveAsync(document);
}
}
I know that some languages implement continuations natively, via call-with-current-continuation (callcc
), but I don't really understand how that works or what it does exactly.
So here's the question: if Anders et al. had decided to bite the bullet and just implement callcc
in c# 5.0 instead of the async
/await
special case, what would the above snippet look like?
Async methods are intended to be non-blocking operations. An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method.
With the new C++11 standard, there is std::async . Pretty much anything the machine is capable of can be done in C, you just have to do it yourself.
Asynchronous programming can in some cases help with performance by parallelizing a task. But, that is not its main benefit in day to day development. Instead, the main benefit comes from making our code more scalable. The scalability feature of a system relates to how it handles a growing amount of work.
A significant benefit of the async/await pattern in languages that support it is that asynchronous, non-blocking code can be written, with minimal overhead, and looking almost like traditional synchronous, blocking code.
Your question, as I understand it, is "what if instead of implementing "await" specifically for task-based asynchrony, rather, the more general control flow operation of call-with-current-continuation had been implemented?"
Well, first of all let's think about what "await" does. "await" takes an expression of type Task<T>
, obtains an awaiter, and calls the awaiter with the current continuation:
await FooAsync()
becomes effectively
var task = FooAsync();
var awaiter = task.GetAwaiter();
awaiter.BeginAwait(somehow get the current continuation);
Now suppose we had an operator callcc
which takes as its argument a method, and calls the method with the current continuation. That would look like this:
var task = FooAsync();
var awaiter = task.GetAwaiter();
callcc awaiter.BeginAwait;
In other words:
await FooAsync()
is nothing more than
callcc FooAsync().GetAwaiter().BeginAwait;
Does that answer your question?
As a commenter points out, the answer below assumes the code generation pattern from the "Technology Preview" version of the async/await feature. We actually generate slightly different code in the beta version of the feature, though logically it is the same. The present codegen is something like:
var task = FooAsync();
var awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
awaiter.OnCompleted(somehow get the current continuation);
// control now returns to the caller; when the task is complete control resumes...
}
// ... here:
result = awaiter.GetResult();
// And now the task builder for the current method is updated with the result.
Notice that this is somewhat more complicated, and handles the case where you are "awaiting" a result that has already been computed. There's no need to go through all the rigamarole of yielding control to the caller and picking up again where you left off if the result that you are waiting for is in fact already cached in memory for you right there.
Thus the connection between "await" and "callcc" is not quite as straightforward as it was in the preview release, but it is still clear that we are essentially doing a callcc on the "OnCompleted" method of the awaiter. We just don't do the callcc if we don't have to.
As this answer
https://stackoverflow.com/a/9826822/88656
from Timwi points out, the semantics of call/cc and await are not quite the same; a "true" call/cc requires either that we "capture" the entire continuation of a method including its whole call stack, or equivalently that the whole program be rewritten into continuation passing style.
The "await" feature is more like a "cooperative call/cc"; the continuation only captures "what is the current task-returning method about to do next at the point of the await?" If the caller of the task-returning method is going to do something interesting after the task is complete then it is free to sign up its continuation as the continuation of the task.
I'm no expert at continuations, but I'll take a stab at explaining the difference between async/await and call/cc. Of course this explanation assumes I understand call/cc and async/await, which I'm not sure I do. Nevertheless, here goes...
With C# 'async', you are telling the compiler to generate a special version of that specific method which understands how to bottle it's state into a heap-data-structure, so it can be "removed from the real stack" and resumed later. Within an async-context, "await" is then like "call/cc" in that it uses the compiler-generated objects to bottle up the state and get off the "real stack" until the task completes. However, because it's the compiler rewriting of an async-method that allows the state to be bottled up, await can only be used within an async context.
In first-class call/cc, the language runtime generates all code such that the current continuation can be bottled up into a call-continuation-function (making the async keyword unnecessary). call/cc still acts like await, causing the current-continuation state (think of the stack state) to be botled up and passed in as a function to the called-function. One way to do this is to use heap-frames for all function invocation, instead of 'stack' frames. (sometimes referred to as 'stackless', as in 'stackless python' or many scheme implementations) Another way is to remove all the data from the "real stack" and stuff it into a heap data-structure before calling the call/cc target.
Some tricky issues can arise if there are calls to external functions (think DllImport) intermixed on the stack. I suspect this is the reason they went with the async/await implementation.
http://www.madore.org/~david/computers/callcc.html
Because in C#, a function must be marked as "async" in order to use these mechanics, I wonder if this async keyword will become a virus that spreads to lots of functions in lots of libraries. If this happens. they may eventually realize they should implement first-class call/cc at the VM level, instead of this compiler-rewriting based async model. Only time will tell. However, it's certainly a useful tool in the context of the current C# environment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With