So, I just published an update to my testbench crate, whose goal is to collect in a single place the various simple and reasonably general-purpose testing and benchmarking primitives which I have needed so far over the course of experimenting with thread synchronization protocols in Rust. The main purpose of this release is to introduce a new, rather evil testing primitive, that I think will prove quite useful to poor souls like me who try to test low-level multithreaded code: the RaceCell.
The basic interface to a RaceCell is very simple: like a Cell, a RaceCell is designed to hold a value of a certain type U, and to allow you to read (load) and change (store) the inner value at will given only a regular reference to it. Unlike a Cell, however, a RaceCell can be shared between threads (i.e. it is Sync), with the following purposely peculiar semantics:
- Stores to a RaceCell are guaranteed not to be atomic. They are performed in at least two hardware operations, which would be very unlikely to be merged into a single memory transaction by any reasonable compiler or execution platform.
- Moreover, a RaceCell can use this non-atomicity to detect and report the usage scenario where a load executes concurrently with a store, and observes a half-committed RaceCell state.
Together, these two properties mean that a RaceCell allows you to easily trigger and detect race conditions which, for all intent and purposes, look and feel like data races. They are technically not data races in Rust’s sense of the word, because that would be Undefined Behaviour, and I have tested in a previous draft of RaceCell which featured an actual data race that as with all undefined behaviour, optimizing compilers will helpfully make a random, ridiculously meaningless mess of it. But they look well enough like these to enable similar reasoning.
Now, what would you do with such a ridiculous container? Well, test thread synchronization primitives, of course! When building concurrent data structures holding values of an arbitrary unsynchronized data type T, it is very important to make sure that these values are never exposed to the client in a half-written state, through appropriate use of memory fences. If you make your data structure hold RaceCells, you can check that this is the case: given a correct synchronization protocol, clients should only observe the RaceCells in a consistent state, since they can only load their state after the producer has, for all intents and purposes, completed any pending stores to them.
Hope someone else will find this useful, personnally I’ll use this to improve my unit tests on the data synchronization protocol used by triple_buffer and spmc_buffer.