The Signal Arm Dijkstra Borrowed
The prototype railways solved a lethal concurrency problem a century before computers had one: two trains, one piece of track, no way to undo a collision. Their answer was to chop the line into blocks and guard each with a signal — a semaphore, the pivoting arm you still see on old branch lines. A block holds one train. The signal behind an occupied block drops to danger; clear the block and the arm lifts, letting the next train proceed.
When Edsger Dijkstra needed a name for the primitive that admits one process into a critical section and makes the rest wait, he took the railway’s word wholesale. His semaphore is the signal arm; the critical section is the block; P and V are a train claiming and releasing the track.
Wiring current-sensing occupancy detection into my flea-market HO switcher’s oval this afternoon — a block that knows a train is sitting on it — I was building the same interlock in miniature. So here it is in code: one block, two trains racing to enter.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let block = Arc::new(Mutex::new(())); // one block, one occupant
let trains: Vec<_> = ["Local", "Express"].map(|id| {
let block = Arc::clone(&block);
thread::spawn(move || {
let _token = block.lock().unwrap(); // signal drops to danger
println!("{id} occupies the block");
}) // guard dropped here: arm lifts
}).into_iter().collect();
for t in trains { t.join().unwrap(); }
}
A Rust Mutex is a binary semaphore with its counter pinned at one. lock() is P; the guard’s Drop is V — the arm lifts automatically when the token leaves scope, the railway’s fail-safe rendered as a type.
import Control.Concurrent
main :: IO ()
main = do
block <- newMVar () -- token present: block is clear
done <- newEmptyMVar
let enter name = do
takeMVar block -- claim it (P)
putStrLn (name ++ " occupies the block")
putMVar block () -- clear the signal (V)
putMVar done ()
mapM_ (forkIO . enter) ["Local", "Express"]
mapM_ (const (takeMVar done)) [1, 2 :: Int]
Haskell’s MVar () is the same token from the other side: a box holding exactly one value. takeMVar empties it; a thread that finds it empty waits on the platform until someone puts the token back. Both programs print one train, then the other — never overlapping, because the token can’t be in two places:
Local occupies the block
Express occupies the block
The block on my layout enforces it in hardware — the arm can’t lift until the wheels leave the rail. The code enforces it with a value only one thread can hold. Same interlock, cheaper collisions.