Actors, Green Threads and CSP On The JVM – No, You Can't Have A Pony

I really wish people would stop building actor frameworks for the JVM. I know, I'm guilty of having done this myself in the past. Invariably, these projects fall far short of their intended goals, and in my opinion the applications which adopt them end up with a worse design than if they had never incorporated them in the first place.

Let's take a step back, however. What the hell are actors, and why is everyone so hot and bothered by them? The actor model describes a set of axioms to be followed in order to avoid common issues with concurrent programming, and in the academic world it provides a means for the theoretical analysis of concurrent computation. Specific implementations can vary substantially in how they define actors, and in the restrictions on what actors can and cannot do, however the most basic axioms of the actor model are:

  1. All actor state is local to that actor, and cannot be accessed by another.
  2. Actors must communicate only by means of message passing. Mutable messages cannot be aliased.
  3. As a response to a message an actor can: launch new actors, mutate its internal state, or send messages to one or more other actors.
  4. Actors may block themselves, but no actor should block the thread on which it is running.

So what are the advantages to adopting the actor model for concurrent programming? The primary advantages center around the ergonomics of concurrency. Concurrent systems are classically very hard to reason about because there are no ordering guarantees around memory mutation beyond those which are manually enforced by the programmer. Unless a lot of care, planning and experience went into the design of the system, it inevitably becomes very difficult to tell which threads might be executing a given piece of code at a time. The bugs that crop up due to sloppiness in concurrency are notoriously difficult to resolve due to the unpredictable nature of thread scheduling. Stamping out concurrency bugs is a snipe hunt.

By narrowing the programming model so drastically, actor systems are supposed to avoid most of the silliness encountered with poorly designed concurrency. Actors and their attendant message queues provide local ordering guarantees around delivery, and since an actor can only respond to a single message at a time you get implicit locking around all of the local state for that actor. The lightweight nature of actors also means that they can be spawned in a manner that is 1:1 with the problem domain, relieving the programmer of the need to multiplex over a thread pool.

Actor aficionados will probably reference performance as an advantage of actor frameworks. The argument for superior performance of actors (and in particular the green thread schedulers that most actor implementations are built upon) comes down to how a server decomposes work from the client and how that work gets executed on a multi-core machine. The typical straw-man drawn up by actor activists is a message passing benchmark using entirely too many threads, run on a cruddy macbook. It's easy to gin up some hackeneyed FUDagainst threads to market an actor framework. It's much harder to prove a material advantage to adopting said framework.

Unfortunately, actor frameworks on the JVM cannot sufficiently constrain the programming environment to avoid the concurrency pitfalls that the actor model should help you avoid. After all, within the thread you are simply writing plain old java (or scala or clojure). There's no real way to limit what that code can do, unless it is explicitly disallowed from calling into other code or looping. Therefore, even the actor frameworks which use bytecode weaving to implement cooperative multi-tasking amongst actors cannot fully guarantee non-blocking behavior. This point bears repetition: without fundamental changes in how the JVM works, one cannot guarantee that an arbitrary piece of code will not block.

When making engineering decisions we must always be mindful of the tradeoffs we make and why we make them. Bolt on actor systems are complex beasts. They often use bytecode weaving to alter your code, hopefully without altering its meaning. They quite often rely on Java's fork/join framework, which is notorious for its overhead, especially when it comes to small computations, and is fantastically complicated when compared to a vanilla thread pool. Actor systems are supposed to make parallel computation dead simple, but every lightweight threading system on the JVM that I've seen is anything but simple.

Lest you think that I am a hater, I genuinely like actor oriented programming. I have been an enthusiastic Erlang programmer for a number of years, and I used to get genuinely excited about the activity around adding this paradigm to Java. However, I am now convinced that without support from the platform these lightweight concurrency libraries will always be a boondoggle. I'm not the only one to make this observation, either.

We shouldn't be trusting vendors who are pushing manifestos, decades old tribal knowledge about thread implementations, and misleading benchmarks. We should be building the simplest possible systems to solve our problems, and measuring them to understand how to get the most out of our machines.