28 June 2013
core.async is a new contrib library for Clojure that adds support for asynchronous programming using channels.
There comes a time in all good programs when components or subsystems must stop communicating directly with one another. This is often achieved via the introduction of queues between the producers of data and the consumers/processors of that data. This architectural indirection ensures that important decisions can be made with some degree of independence, and leads to systems that are easier to understand, manage, monitor and change, and make better use of computational resources, etc.
On the JVM, the java.util.concurrent package provides some good concurrent blocking queues, and they are a viable and popular choice for Clojure programs. However, in order to use the queues one must dedicate one or more actual threads to their consumption. Per-thread stack allocation and task-switching overheads limit the number of threads that can be used in practice. Another limitation of j.u.c. queues is there is no way to block waiting on a set of alternatives.
Thread overheads or lack of threads often cause people to move to systems based upon events/callbacks, in the pursuit of greater efficiency (often under the misnomer 'scalability', which doesn’t apply since you can’t scale a single machine). Events complect communication and flow of control. While there are various mechanisms to make events/callbacks cleaner (FRP, Rx/Observables) they don’t change their fundamental nature, which is that upon an event an arbitrary amount of other code is run, possibly on the same thread, leading to admonitions such as "don’t do too much work in your handler", and phrases like "callback hell".
The objectives of core.async are:
To provide facilities for independent threads of activity, communicating via queue-like channels
To support both real threads and shared use of thread pools (in any combination), as well as ClojureScript on JS engines
To build upon the work done on CSP and its derivatives
It is our hope that async channels will greatly simplify efficient server-side Clojure programs, and offer simpler and more robust techniques for front-end programming in ClojureScript.
In modern incarnations, the notion of a channel becomes first class, and in doing so provides us the indirection and independence we seek.
A key characteristic of channels is that they are blocking. In the most primitive form, an unbuffered channel acts as a rendezvous, any reader will await a writer and vice-versa. Buffering can be introduced, but unbounded buffering is discouraged, as bounded buffering with blocking can be an important tool coordinating pacing and back pressure, ensuring a system doesn’t take on more work than it can achieve.
core.async is a library. It doesn’t modify Clojure. It is designed to support Clojure 1.5+.
You can create a channel with the chan function. This will return a channel that supports multiple writers and readers. By default, the channel is unbuffered, but you can supply a number to indicate a buffer size, or supply a buffer object created via buffer, dropping-buffer or sliding-buffer.
The fundamental operations on channels are putting and taking values. Both of those operations potentially block, but the nature of the blocking depends on the nature of the thread of control in which the operation is performed. core.async supports two kinds of threads of control - ordinary threads and IOC (inversion of control) 'threads'. Ordinary threads can be created in any manner, but IOC threads are created via go blocks. Because JS does not have threads, only
go blocks and IOC threads are supported in ClojureScript.
go is a macro that takes its body and examines it for any channel operations. It will turn the body into a state machine. Upon reaching any blocking operation, the state machine will be 'parked' and the actual thread of control will be released. This approach is similar to that used in C# async. When the blocking operation completes, the code will be resumed (on a thread-pool thread, or the sole thread in a JS VM). In this way the inversion of control that normally leaks into the program itself with event/callback systems is encapsulated by the mechanism, and you are left with straightforward sequential code. It will also provide the illusion of threads, and more important, separable sequential subsystems, to ClojureScript.
There are analogous operations for use on ordinary threads - >!! (put blocking) and <!! (take blocking), which will block the thread on which they are called, until complete. While you can use these operations on threads created with e.g. future, there is also a macro, thread, analogous to
go, that will launch a first-class thread and similarly return a channel, and should be preferred over
future for channel work.
You can put on a channel from either flavor of
>!! and similarly take with either of
<<! in any combination, i.e. the channel is oblivious to the nature of the threads which use it.
It is often desirable to be able to wait for any one (and only one) of a set of channel operations to complete. This powerful facility is made available through the alts! function (for use in
go blocks), and the analogous alts!! (alts blocking). If more than one operation is available to complete, one can be chosen at random or by priority (i.e. in the order they are supplied). There are corresponding alt! and alt!! macros that combine the choice with conditional evaluation of expressions.
Timeouts are just channels that automatically close after a period of time. You can create one with the timeout function, then just include the timeout in an
alt variant. A nice aspect of this is that timeouts can be shared between threads of control, e.g. in order to have a set of activities share a bound.
As with STM, the pervasive use of persistent data structures offers particular benefits for CSP-style channels. In particular, it is always safe and efficient to put a Clojure data structure on a channel, without fear of its subsequent use by either the producer or consumer.
core.async has obvious similarities to Go channels. Some differences with Go are:
All of the operations are expressions (not statements)
This is a library, not syntax
alts! is a function (and supports a runtime-variable number of operations)
Priority is supported in
Finally, Clojure is hosted, i.e. we are bringing these facilities to existing platforms, not needing a custom runtime. The flip-side is we don’t have the underpinnings we would with a custom runtime. Reaching existing platforms remains a core Clojure value proposition.
I remain unenthusiastic about actors. They still couple the producer with the consumer. Yes, one can emulate or implement certain kinds of queues with actors (and, notably, people often do), but since any actor mechanism already incorporates a queue, it seems evident that queues are more primitive. It should be noted that Clojure’s mechanisms for concurrent use of state remain viable, and channels are oriented towards the flow aspects of a system.
Note that, unlike other Clojure concurrency constructs, channels, like all communications, are subject to deadlocks, the simplest being waiting for a message that will never arrive, which must be dealt with manually via timeouts etc. CSP proper is amenable to certain kinds of automated correctness analysis. No work has been done on that front for core.async as yet.
Also note that async channels are not intended for fine-grained computational parallelism, though you might see examples in that vein.
Networks channels and distribution are interesting areas for attention. We will also being doing performance tuning and refining the APIs.
I’d like to thank the team that helped bring core.async to life:
And once again, Tom Faulhaber for his work on autodoc.
While the library is still in an early state , we are ready for people to start trying it out and giving us feedback. The CLJS port is still work in progress. Please have a look at the examples, which we will expand over time.
It should be noted that the protocols behind the implementation should still be considered an implementation detail for the time being, until we finish our exploratory work around network channels, which might impact their design.
I hope that these async channels will help you build simpler and more robust programs.