12 — Concurrency and parallelism¶
One line: Ruby has four units of execution — the thread, the fiber, the Ractor and the process — and each page here starts one, drives it to a fixed end, and prints only what that end is, beside the Python tool that does the same job.
A Ruby thread is a real OS thread that shares every object with the others and takes turns with them under the Global VM Lock, which is CPython's GIL under another name; the same three tools — a mutex, a queue, a condition variable — make shared state safe in both languages, and the same ideas fail in both when they are missing. The rest of the chapter is where a Python programmer's model needs adjusting. A Fiber is a coroutine resumed by hand, the machine under a Python generator, but exposed as an object; Enumerator#next runs on one, which is why Thread.current[:x] — a fiber-local despite its name — vanishes inside it. A Ractor is Ruby's answer to the GVL: a block that runs in parallel because it can reach only frozen objects and copies everything else, the line Python draws one process further out with multiprocessing. And fork, spawn, system and Process.wait are the Unix calls with $? attached, where Python has os.fork, os.waitpid and subprocess. The last page takes the chapter to production: the "queue full" page an overloaded Rails site shows is a SizedQueue refusing, and a simulation of Passenger's pool and queue shows why refusing beats waiting, and where HAProxy's 504 comes from instead.
Every program here is deterministic on purpose. Threads are joined before anything about them is printed, results come back through a Queue or an array under a Mutex and are sorted, every timeout is a fifth of a second set against a five-second sleep, and no page prints a pid, a timing or an interleaving. What that costs is the one claim these pages cannot make from a run: that Ractors, unlike threads, use more than one core. That claim stays in prose, marked as such.
| Lesson | Level | The one thing |
|---|---|---|
| Threads are OS threads that take turns under the GVL | 301 | join re-raises, value returns the block's last expression, and a finishing main thread kills the rest — where Python waits for them |
| Mutex guards, Queue hands over, ConditionVariable waits | 301 | ten threads adding 1 a thousand times inside synchronize give exactly 10000; a closed Queue ends the consumer with nil |
| A Fiber is a coroutine you resume by hand | 301 | values go in through resume and out through Fiber.yield, and nothing else moves control — a generator's send and yield |
Enumerator#next runs the block on its own Fiber |
301 | to_a runs the block on the calling fiber, next on a private one; a Python generator is a suspended frame on the same thread |
Thread.current[:x] is fiber-local, not thread-local |
301 | a value set with Thread.current[] is nil inside Enumerator#next; thread_variable_get and Fiber[] are the other two stores |
| Ractors share nothing that is not frozen | 301 | a Ractor copies every unfrozen argument and message, refuses an outer local, and wraps an inner exception in Ractor::RemoteError |
fork copies the process; wait collects its status |
301 | fork returns nil in the child and a pid in the parent, a signalled child has no exitstatus, and system returns true, false or nil |
Timeout.timeout interrupts a thread from outside |
301 | a watcher thread calls Thread#raise on yours; ensure still runs, and Python can only time out a wait, never a thread |
A SizedQueue that refuses is the 503 behind "queue full" |
201 | Passenger's pool of 6 and queue of 100 fill in ten seconds at 40 requests/s, and refusing keeps the wait under four seconds where an uncapped queue reaches twenty; SizedQueue#push(job, true) raises ThreadError: queue full |
Read more¶
- Thread ↗ — the class reference, with the GVL under Thread and the fiber-local
[]documented as such - Fiber ↗ —
resume,Fiber.yield,kill, storage and the scheduler hook - Ractor ↗ — shareable objects, ports,
valueand the errors - Process ↗ —
fork,spawn,wait,wait2andProcess::Status - Timeout ↗ — how
Timeout.timeoutworks and why it is dangerous, in its own words - threading ↗ —
Thread,Lock,RLock,Condition,Eventandlocal - queue ↗ —
Queue,task_doneandjoin - multiprocessing ↗ — processes, queues, pipes and the start methods
- subprocess ↗ —
run,PopenandCalledProcessError - contextvars ↗ — Python's counterpart of
Fiber[]