Rust: regret-less concurrency
By: Gregory Terzian
Tags: Rustlang, JavaScript, Rust

The beauty of is that, while the HTML event-loop is fairly convoluted, the concept of an event-loop itself is very simple, and you can use it to model your concurrent Rust code, starting simple and making it as complicated as your problem requires.

The native thread: a simple building block.

To go about building our own event-loops in Rust, we need a simple way to represent a “running component”. The thread, a.k.a “native thread”, is a good bet.

Threads sometimes get a bad rap for being “heavy-weight”, with people writing that they are running millions of a certain flavor of “lightweight threads” on a single machine, whereas this particular feat of engineering could never be performed with “native” threads.

When I read for example “Why you can have millions of Goroutines but only thousands of Java Threads”, I wonder, “who said that threads should be the basic unit of computation?”. If you think of a thread as a “running component” that can handle many “units of computations”, with the “unit” being a event/message, there is no need to spawn millions of them to handle, say, millions of events. Instead you’ll spawn about one thread per “logical component”, and let them handle as many events as sensible.

I can only speak for myself but I think that my brain can only handle about 25 to 50 logical components, and even on a good day, that is stretching it. Your OS will have no problems handling hundreds, even thousands, of threads. One could therefore say that the number of threads is less bounded by the capabilities of the OS to context switch between them, but rather by your brain’s capability to context switch on the logic represented by them.

Finally, and here I’m jumping ahead, when I write that a component can be “represented” as a thread, I don’t mean that it absolutely must consist of only a single thread. If needed, one such component could own a thread-pool, or a “lightweight task executor” to perform internal computations(the emphasis being put on “internal”).

Channels: your source of events

If threads represent a “component”, how can we represent “events”, and “loop” over incoming ones? Enter: channels.

You can think of a channel as a thread-safe queue of events, with component using those to enqueue events, in a form of message-passing, on each others event-loops. Note that “thread-safe” is only in the eye of the beholder here. From the perspective of one component’s event-loop, the others components are running concurrently. Yet each component is actually running a single-threaded, sequential, event-loop.

Time for an example…

Let’s take a look at a real-word concurrent component running it’s own “event-loop”: the background-hang-monitor in Servo.

As you can see from the snippet above, we have a BackgroundHangMonitorWorker “running until it drops” in a native thread.

We can also see that it makes available to the world a HangMonitorRegister , which appears to be a wrapper around a channel sender.

It also seems that another channel sender, called constellation_chan is being shared with the worker.

So far this follows the mantra of “communicate by sharing your sender”…

First, a word on what that component is supposed to be doing: its purpose is to monitor other components for “hangs”, and whenever one such “hangs” is noticed, alert another component, called the “constellation”. More details on it can be read in a previous article.

How do we “monitor” other components? Should we share some state and keep an eye on it in a loop? Nope, components will simply send messages to the monitor, and the monitor can infer the activity/hanging of component from the messages it (does not) receive(s).

The above is essentially the “event-loop” of the monitor. A few features stand out: we’re select ‘ing not only over our receiver, but also over a crossbeam_channel::::after , introducing a useful concept of sending ourselves a message if nothing comes in from monitored components. It’s useful because as you can imagine, we can’t expect hanging components to send us a message telling us to check up on them when they are hanging…

Also, we add a little while loop to drain the channel at each iteration. The thinking behind that is: since we end-up doing some fairly heavyweight sampling of hanging components in some cases, the draining of the channel at each iteration hopefully reduces spurious hangs that could emerge from a “one message, one checkpoint, per iteration” model.

How do things look like from the perspective of a “monitored” component?

Equivalent of https://github.com/servo/servo/blob/be84644bc0b854bc04e6eb4c2e300b1a8e59a234/components/layout_thread/lib.rs#L639

As you can see, this monitored component is also running an event-loop.

The difference is that before blocking on the select , the component will send a message to the hang monitor, notifying it that it will start “waiting” for an event to come in(meaning that idleness shouldn’t be interpreted as a “hang”). When an event comes in, the component will notify the start of a new event handling by sending another message(meaning that if the monitor doesn’t hear back from the component for a while, it is apparently hanging on handling that last event).

A note on distributed systems

Some of you might have thought that the “background hang monitor” described above appears to be “tailing the logs” of monitored components, with the messages sent by components representing the logs.

If you look at the use cases for something like Kafka, you’ll see something fairly close to “message passing” across event-loops as well.

The point is that the event-loop is a simple, yet versatile abstraction, that turns out to be useful in a variety of settings besides when using multiple threads, such as when using multiple processes, and even when doing stuff over the network.

What if it’s more complicated?

What if your component needs to run some complicated parallel algorithm in response to an event(requiring “data parallelism”, as opposed to the “task parallelism” described earlier in the form of parallel event-loops)? How can you model that? As I hinted at earlier, modelling your component as a thread running an event-loop doesn’t mean that it must consist of only a single thread. Your component, while running inside a thread, could still own, or at least have access to, other constructs, such as a thread-pool, on which to run the complicated parallel algorithm.

Your event-loop could, in response to an event, “spawn” all sorts of internal computations. Those could block the event-loop, meaning that no events are handled while the computation is in progress, or it could happen “in-parallel” of it, meaning that event will keep being handled(and perhaps more parallel stuff being spawned). One “event-loop” could, for what it’s worth, even run an entire sub-system of event-loops...

Once again, I couldn’t put it better than the HTML living standard: