Rsevents: Manual and Auto Reset Events for Rust

Print   952 words   2018-12-08

One of the unique characteristics of Rust (both the language and the community that has evolved around it) is a strong acknowledgement of multithreading, synchronization, and concurrency, as witnessed in the design of the core language (which acknowledges OS concepts of threads with sync and send) and the presence of various structures in the standard library aimed at simplifying development of correct, multithreaded code.

rsevents is a new crate that should be immediately familiar to anyone that has done multithreaded programming under Windows: it exposes a synchronization primitive, namely, an event for use where Mutex – intended to exclusively marshall access to a variable or region of code – either does not convey the correct semantics or is not the right tool for the job. For those that didn’t come fleeing to rust from a Win32 background, in Windows an event is the lowest-level kernel synchronization primitive, which can be thought of as a “waitable bool” – it is either set or reset (on or off, true or false) and if it isn’t set, you can wait on it until it becomes set.

An event is used for (unstructured) synchronization of state, a lot like an anonymous, 1-bit Channel but simultaneously much more flexible (but trickier!) as it is a primitive. To naively implement events in rust (or in posix), it would be the equivalent of a conditional variable and a mutex, where the mutex is used only to protect the integrity of the conditional variable (and not to otherwise indicate a critical section), only without spurious wakes. Win32 events (and rsevents) come in two different variants, a so-called “manual reset” event and the (undoubtedly more important) “auto reset” event. To continue with the posix parallel, the behavior is the difference between using pthread_cond_broadcast and pthread_cond_signal – it either wakes up all waiting listeners or only one, thereby guaranteeing atomicity.

rsevents implements these as rsevents::AutoResetEvent and rsevents::ManualResetEvent, both impls of the trait rsevents::Awaitable, atop of the excellent ParkingLot crate for the best sleep/wake performance rust currently has to offer, with minimal overhead to boot. As the docs illustrate, they are dead-simple to use and allow one to quickly get started with easily polling or awaiting state changes:

// create a new, initially unset event
let event = AutoResetEvent::new(State::Unset);

// and wrap it in an ARC to allow sharing across thread boundaries
let event = Arc::new(event);

// create a worker thread to complete some task
{
	let shared = event.clone();
	std::thread::spawn(|| {
		// perform some task
		...
		// and signal to ONE waiting thread that the result is ready
		shared.set();
	});
}

...

// wait for the spawned thread to finish the queued work
event.wait();

rsevents only represents the core underpinnings of event-based synchronization – as mentioned repeatedly, these are synchronization primitives and while they may be useful on their own to communicate changes in (one-bit) state, they are actually more useful to create Awaitable objects (hence the separation between the events and the Awaitable interface). This is where the true power of rsevents is exposed: it lets you build (or return) objects on top of the events, which become asynchronously awaitable.

To get the ball rolling, a separate crate is also being introducing: rsevents-extra which contains helpful synchronization objects built atop of rsevents all of which implement rsevents::Awaitable. One example of an object implementing such behavior is CountdownEvent which implements an awaitable reverse counter that can be used to wait until n tasks have finished: you initialize it some value, decrement it (calling CountdownEvent::tick()) until it reaches zero, at which point any threads waiting on the object (via CountdownEvent::wait(), courtesy of rsevents::Awaitable) are awoken:

let countdown = CountdownEvent::new(42);

thread::scope(|scope| {
    let worker_thread = scope.spawn(|_| {
        println!("Worker thread reporting for duty!");

        // block here until all the countdown has elapsed
        countdown.wait();

        println!("Worker thread can now do something because the countdown finished!");
    });

    for i in 0..1000 {
        if i % 7 == 3 {
            countdown.tick();
        }
    }
});

It’s a pretty silly example that doesn’t do the crate justice, but countdown events are a great way of performantly waiting for an external event predicated on n things taking place, and will outperform other alternatives that involve mutating shared state, while simultaneously abstracting away the process of actually waiting on the state to change.

In my days of Win32 C/C++ programming, I had written an entire library of single-file headers each implementing a different synchronization model and built directly atop of auto or manual reset events. Rust has obviated quite a few of them, but there’s a lot to be said for the flexibility that events have to offer (and their very favorable performance characteristics). rsevents-extra is intended to be a community project and is accepting pull requests for other useful synchronization objects implementing Awaitable, so if you have any good ideas, we’re listening!