WeSearch

Ruby Concurrency: What Happens

·17 min read · 0 reactions · 0 comments · 1 view
Ruby Concurrency: What Happens

Every 'what happens when' question about Ruby concurrency, answered with diagrams.

Original article
Carmine Paolino
Read full at Carmine Paolino →
Full article excerpt tap to expand

Since I wrote about async Ruby and patched Solid Queue to support fibers, people keep asking the same questions. What happens when a fiber blocks? Don’t you still need threads? What about database transactions? What about Ractors? This post answers all of it. From the ground up. The four primitives Ruby gives you four concurrency primitives: processes, threads, fibers, and Ractors. They nest. Every process has an implicit “main Ractor” where your code runs by default, so you never have to think about Ractors unless you explicitly create one. Without Ractors, the hierarchy is simply process – threads – fibers. With Ractors, it becomes: graph TD P[Process] --> R1["Ractor 1 (GVL 1)"] P --> R2["Ractor 2 (GVL 2)"] R1 --> T1[Thread 1] R1 --> T2[Thread 2] R2 --> T3[Thread 3] T1 --> F1[Fiber A] T1 --> F2[Fiber B] T2 --> F3[Fiber C] T3 --> F4[Fiber D] T3 --> F5[Fiber E] style P fill:#4a90a4,color:#fff style R1 fill:#c084fc,color:#fff style R2 fill:#c084fc,color:#fff style T1 fill:#7fb069,color:#fff style T2 fill:#7fb069,color:#fff style T3 fill:#7fb069,color:#fff style F1 fill:#e8a87c,color:#fff style F2 fill:#e8a87c,color:#fff style F3 fill:#e8a87c,color:#fff style F4 fill:#e8a87c,color:#fff style F5 fill:#e8a87c,color:#fff Think of your computer as an office building. Processes are fully isolated: separate offices, each with its own locked door, furniture, and files. Each process has its own memory, its own Ruby VM, and its own GVL. When you run Puma with 3 workers, you get 3 processes. They can’t corrupt each other’s state because they don’t share memory. The OS schedules them independently. The cost: each one loads your entire application into memory. Ractors sit between processes and threads: offices that share a mailroom but not their filing cabinets. Each Ractor has its own GVL, so threads in different Ractors can execute Ruby code truly in parallel, but they can only pass notes to each other – no shared mutable objects. You communicate via message passing, copying or moving data between them. Every Ruby process has a “main Ractor” where all your code runs by default. Creating additional Ractors is opt-in. Threads live inside a process and share its memory: workers sharing the same office, accessing the same filing cabinets, coordinating to avoid collisions. The OS preemptively schedules them, meaning it can pause any thread at any time and switch to another. You don’t control when this happens. The GVL prevents threads from executing Ruby code in parallel, but it releases the lock during I/O. So two threads can wait on two different network calls simultaneously, but they can’t crunch numbers at the same time. Fibers live inside a thread and are cooperatively scheduled: multiple tasks juggled by one worker at their desk. When they’re waiting for something – a phone call, a fax, a response – they set it aside and pick up the next task. A fiber runs until it explicitly yields. When it hits I/O – a network call, a database query, reading a file – it yields to the reactor, and another fiber picks up. No OS thread context switch for the fiber itself, no preemption. One thread can run thousands of fibers. Here’s what that means for cost: Process Ractor Thread Fiber Memory full app copy ~thread + Ractor state ~8MB virtual stack reservation ~4KB initial virtual stack, grows as needed Creation time ~ms ~80μs ~80μs ~3μs Context switch kernel kernel (threads within) ~1.3μs (kernel) ~0.1μs (userspace) Isolation Full (own memory) Share-nothing…

This excerpt is published under fair use for community discussion. Read the full article at Carmine Paolino.

Anonymous · no account needed
Share 𝕏 Facebook Reddit LinkedIn Email

Discussion

0 comments

More from Carmine Paolino