Le GVL : un seul thread exécute du Ruby — l'attente, pas le calcul, se chevauche.The GVL: one thread runs Ruby — waiting, not computing, overlaps.
Le dernier coût, et le plus mal compris. En MRI — l'implémentation de référence — un verrou global, le GVL, garantit qu'un seul thread exécute du code Ruby à la fois. Conséquence frontale : des threads ne parallélisent pas le calcul. Lancer quatre threads sur un calcul CPU ne va pas quatre fois plus vite ; le GVL les fait passer chacun leur tour. Mais le GVL est relâché pendant les attentes — un appel réseau, une lecture disque, un sleep. Là, les threads brillent : pendant qu'un thread attend la réponse d'un serveur, un autre peut lancer sa requête. La question n'est donc jamais « threads ou pas » dans l'absolu, mais « mon goulot est-il de l'attente ou du calcul ? ».The last cost, and the most misunderstood. In MRI — the reference implementation — a global lock, the GVL, guarantees that only one thread runs Ruby code at a time. The frontal consequence: threads don't parallelize computation. Launching four threads on a CPU task doesn't go four times faster; the GVL takes them in turns. But the GVL is released during waits — a network call, a disk read, a sleep. There, threads shine: while one thread waits for a server's response, another can fire its request. So the question is never 'threads or not' in the absolute, but 'is my bottleneck waiting or computing?'.
# en MRI, le GVL (Global VM Lock) : UN seul thread exécute du Ruby à la fois def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2) # CPU-bound : 4 threads ne vont PAS plus vite — le GVL les sérialise ts = 4.times.map { Thread.new { fib(33) } } ts.each(&:join) # ≈ aussi lent que 4 fib(33) en série # I/O-bound : 4 threads VONT plus vite — le GVL est libéré pendant l'attente urls = [...] # 4 requêtes réseau ts = urls.map { |u| Thread.new { Net::HTTP.get(URI(u)) } } ts.map(&:value) # les attentes se CHEVAUCHENT
Le même outil — Thread — accélère un cas et pas l'autre, selon une seule variable : le goulot. Pour de l'attente, les threads chevauchent les temps morts. Pour du calcul, le GVL les met en file, et il faut un autre outil.The same tool — Thread — speeds up one case and not the other, by a single variable: the bottleneck. For waiting, threads overlap the dead time. For computing, the GVL queues them, and you need another tool.
« Mon goulot est-il une attente (threads, async) ou du calcul (Ractors, processus, ou une autre implémentation) ? » C'est la question avant tout choix de concurrence. Et comme tout le volume : on n'y répond pas à l'intuition — on profile pour savoir si le temps part dans le réseau ou dans le CPU."Is my bottleneck a wait (threads, async) or computing (Ractors, processes, or another implementation)?" It's the question before any concurrency choice. And like the whole volume: you don't answer it by intuition — you profile to know whether time goes into the network or the CPU.
Deux notions qu'on confond. La concurrence, c'est gérer plusieurs tâches qui progressent en s'entrelaçant — ce que les threads MRI font très bien pour l'I/O, même sous GVL. Le parallélisme, c'est exécuter plusieurs calculs en même temps, sur plusieurs cœurs — ce que le GVL interdit aux threads, et que seuls les Ractors ou les processus offrent en Ruby. Le mythe « Ruby est lent en concurrence » naît de cette confusion : il est excellent en concurrence d'attente, contraint en parallélisme de calcul.Two notions we confuse. Concurrency is managing several tasks that progress by interleaving — what MRI threads do well for I/O, even under the GVL. Parallelism is running several computations at once, on several cores — what the GVL forbids threads, and only Ractors or processes offer in Ruby. The myth 'Ruby is slow at concurrency' is born of this confusion: it's excellent at wait-concurrency, constrained at compute-parallelism.