Why Events Are A Bad Idea (for High-concurrency Servers)
usenix.org
usenix.org
http://www.hnsearch.com/search#request/submissions&q=Eve...
What I said about it last time:
The problem with the article is that it's superficial. At its core, it's premised on the idea that threads are inherently easier than events, because they produce straight-line code and automatically handle tracking the state needed across a transaction. If that's true, then the article says threads are just as good as events once we solve all the problems that make threads dangerous or slow.
But it's not a given that threads are easy. In particular, as anyone who has spent time doing high-performance threaded code will tell you, there's the effort you have to put in to make sure your threaded code is correct, and then the 2x more time you have to put in to make sure that it's efficient; in particular, you have to do synchronization without effectively serializing all your threads. Which is a huge pain.
I think this is gradually becoming a moot point, as modern languages --- all of which have some kind of anonymous function and some approximation of closures --- allow us to write evented code that looks and feels like straight-line code. jQ callbacks in Javascript are the most vivid illustration of this idea (I suppose the node.js people are carrying that even further; I haven't played with it).
Back in 2002 I did an evaluation of all the open source event-based web servers that were out there for Linux and found that all of them had issues with data corruption. You just couldn't expect to download a 100MB file and get it right every time. Things are better now, but process and thread-based web servers got mature by 1996 if not sooner.
Not getting at you, but I'd question the idea that writing code for distributed systems which looks and feels just like code for a single machine is desirable. My gut says the righteous path looks much more like Erlang, treating concurrent programming as a paradigm in its own right and leaving implementation details up to the compiler.
Like you say, a decent language can make event-driven code seem like straight-line code. The Akka framework (http://akka.io/) even makes event-driven code look like message passing. There are two substantive points to be made, I believe:
- If your language's runtime doesn't allow you to use all available CPUs it's a bit lame -- or at least limited in its application. (Hi Node.js.)
- Given one concurrency abstraction you can, most of the time, build other concurrency abstractions. We've seen this above with Akka. To make this feasible your language better provide some lightweight concurrency mechanism -- either lightweight threads or events.
The problem with threads is that they are more general than a good implementation of events. You've got a lot more rope to hang yourself with.
I think they're neglecting a third option: Shared-immutable state, with very little, explicit, shared-mutable state. This is the model used by Haskell threads, and Erlang processes. Like Events/Cooperative-threads, it does away with the horrible semantics of shared-by-default, and gives the parallelism benefits lost when switching to cooperative multitasking.
But I think there are still 2 crucial differences between event-based systems and cooperative threads that I did not see in the paper:
* Events make it clear where context-switch/reentrancy points are. When I call a library function, then I generally have a very good idea if it could "call me back" (have I given it callbacks to call me back?) and if not -- I know it is safe to call it and do not have to reason about my internal state in every given call. Even with cooperative threads, a "yield" point may be lurking in any library you're calling. This effectively means that any library call can potentially do anything, including switch to "higher-level" code that may re-enter your own code. This makes re-entrancy considerations far more complicated. It is solvable, though, if you make sure that yield-points are "tagged" with some token that must be passed as an argument. Then, a function signature and call pattern tells you whether or not it can yield to anywhere.
* Implementing user-level threads may be very cheap, compared with standard posix threads. But it is still many times more expensive than using explicit data allocations as in event-based programs. Even if using "split stacks", a user-level thread is still going to pay with around a 4Kbyte stack. 1 million threads thus take 4GB of memory before they even do anything. This means that you still have to be wary of creating threads as freely as you would register callbacks -- and requires mixing in event-based programming, or manual event loops, into your threads. This mixture is worse than uniformity of any method.
That sounds good, except passing an argument ("yielder") that cannot be captured/stored (so you have to take it as an argument) sounds like it would require less language magic to solve the same problem.
These small chunks come at a cost, too. The GHC runtime needs to copy around the edges of the stack at the chunk boundaries, and stack use becomes slightly more expensive.
If it was so easy to implement their proposed high performance thread system (disclaimer: haven't read the paper) -- Why don't we see it in production 8 years later?
I'd argue that we don't see it in production, because it lacks some of the advantages they propose in the abstract.
Frankly, the event/thread duality result is actually pretty trivial to grok if you bother to look at it. Hell: with only minimal overhead (due to having started with events and having to adapt up), you can use a continuation passing transform on JavaScript and have a threaded node.js (and some people even do this): you'll have roughly the same concurrency and roughly the same memory usage (and the "roughly", again, only comes from the fact that we are building the threads on top of events with an adapter that will be forced to slightly inefficiently store and access its state).
The node-fibers version has roughly 70% of the throughput of node. Memory consumption was on the order of 28MB for node and 32MB for node-fibers. This is more or less unoptimized code with room for improvement.
There are systems using light-weight threads to write very high concurrency servers. That's what the Erlang VM does, for example. The GHC Haskell runtime now does this as well. They both use epoll/kqueue/etc. in their I/O systems, but the code itself is threaded in user-space, and these lightweight threads can be mapped onto a pool of OS-level threads so the programs can run on multiple cores.
Even without fancy compiler and runtime support, you can do this. In Python, you can use eventlet or gevent to write very concurrent servers with what looks like threaded code, thanks to coroutines in the interpreter. It's very pleasant.
I would guess that one large part of the reason that this is uncommon in production is that fresh CS graduates are not familiar with it. Another reason might be that you can't use this model at all on the client side for some popular clients (like web browsers).
threads +posix +signals
threads +stack +usage
thread +safe +functionHere's a thought experiment: if your language of choice offered threads so lightweight as to be almost free, with proper preemptive scheduling, not limited to a single core, with the I/O using something like epoll or kqueue underneath -- would you use it?
Erlang and Haskell offer this right now. Python can give you a passable approximation. It really is as cool as it sounds.
(In the meantime, I'm happily using eventlet in Python, and some people swear by gevent. These have their shortcomings, but they're worth the trouble, easily.)
Edit: the other big issue here is that blocking operations (e.g. anything in libmysql) are not automatically deferred to a thread pool -- that has to be done manually. Ruby's fibers and Python's greenlets are magical, but not magical enough that you don't occasionally get tackled by fantasy creatures materializing from the aether. If that makes sense.
Being familiar with thread and events makes you a better programmer, so go forth and learn! As I see it, scalable concurrency is far from a solved problem, so it's good to see people pushing both sides of the equation.
So, what hardware and software changes were you thinking of?
While I think, if your choices are the typical thread support and its problems and an event based system then events are the way to go... I believe the great majority of developers have decided to use node or twisted, et. al, either because they'd never heard of erlang, never understood its strength, or weren't willing to deal with the initially alien looking syntax.
I think this is a mistake, but rather than fight it, I'm working on a solution that allows coffeescript to run within a distributed system based on erlang. I've had to provide some work arounds and conventions for the coffeescript developer to follow a sequential programming style, but have the ability for coffeescript to really run concurrently on multiple CPUs or multiple machines, with a common shared state.
(I described some of this here: http://news.ycombinator.com/item?id=2906290 and went into the architecture a bit more here: http://news.ycombinator.com/item?id=2848647 )
Granted, it is easy for me to say it should be done a different way, and until I publish code it is all talk. I'm very aware of this... but I would like to suggest that there really is a difference in erlang, and that events are a compromise that don't inherently actually allow for concurrency. Concurrency is a better solution, isn't that hard to do now (if you write straight up erlang) and I'm working on making it even easier (if you're willing to be constrained a little, bit, but I think the popularity of node.js shows people are willing.)
If it seems I'm shilling for my project, I am a little bit, but I'm also wanting to get it out there so that if I'm making a mistake maybe someone can identify it... or be inspired to build a better way on top of it.
If you want updates, I have a twitter account @nirvanacore that only talks about this project.