Async, async, everywhere
By nico on May 22, 2007
A few weeks ago there was a brief sub-thread on the networking-discuss OpenSolaris list about whether the new proposed kernel sockets API project should not begin by delivering only a synchronous API. That suggestion was quickly dismissed, fortunately.
IMO all APIs that can block on I/O should be asynchronous.
Even APIs that can "block" only on lengthy computation (e.g., crypto) should be async, as such computation might be offloaded to helper devices (thus getting I/O back into the picture) or the call to such an API might fork a helper thread (think automatic parallelism, which one might want on chip multi-threading (CMT) systems) if that is significantly lighter-weight than just doing the computation.
For example, gss_init_sec_context(3GSS) should have an option to work asynchronously, probably using a callback function to inform the application of readiness.
And open(2), creat(2), readdir(3C), and so on should all be asynchronous. If all filesystem-related system calls had async versions then one could build file selection combo box widgets that are responsive even when looking for files in huge directories (the user would see the list of files grow until the whole thing was read in or the file the user was interested in appears, as opposed to having to wait to see anything at all) and which don't need to resort to threading to achieve the same effect. And the NFS requirement that operations like file creation must be synchronous would not penalize clients that support async versions of creat(2) and friends.
Of course, adding async versions of all filesystem-related system calls without resorting to per-call worker threads probably means radical changes to the VFS and the filesystem modules. Which should prove the point: it's much easier to layer sync interfaces atop async ones than it is to rework the implementation of sync interfaces to support async ones efficiently, so one should always start by implementing async interfaces first.
It's been decades since GUI programming taught us that everything must be async. And even web applications, which used to be synchronous because of the way HTML and browsers worked, work asynchronously nowadays -- async is what Ajax is all about.
Really, we should all refrain from developing and delivering any more synchronous blocking APIs without first delivering asynchronous counterparts.
Given a really cheap way to test for the availability of a thread in a CMT environment, and a really cheap way to start one, then all those callback invocations (closure invocations) could be done on a separate thread when one is available.
I like to think of async programming as light-weight threading because I like to think of closures and continuations as light-weight threading. Continuations built on closures and continuation-passing-style (CPS) conversion, in particular (i.e., putting activation records on the heap, rather than on a stack), are a form of very light-weight cooperative multi-threading (green threads): thread creation and context switching between threads has the same cost as calling any function. The trade-off when putting activation records on the heap is that much more garbage is created that needs to be collected -- automatic stack-based memory allocation and deallocation goes out the window. A compromise might be to use delineated continuations and allocate small stacks, with bounds checking rather than guard pages used to deal with stack growth. VM manipulation to setup guard pages and the large stacks needed to amortize this cost are, I suspect, some of the largest costs in traditional thread creation as compared to heap allocation of activation records.
Another reason to think of async as light-weight threading is that a workaround for a missing, but needed, async version of a sync function is to create a worker thread to do the sync call in the background and report back to the caller when the work is done. Threads are fairly expensive to create. At the very least async interfaces allow the implementation cost to be less obvious to the developer and leave more options to the implementor (who might resort to forking a thread per-async call if they really want to).
Finally, pervasive async programming looks a lot like CPS code, which isn't exactly pleasant. Too bad continuations haven't made it as a mainstream high level language feature.