skip to content

In Ruby, what does the concurrent-ruby gem add that core Thread, Mutex and Queue do not, and why do servers and frameworks depend on it?

level: juniorimportance: should knowfreq 35%

answer

  1. core gives primitives, not pools
  2. executors, Promises, Map, atomics
  3. same guarantees on CRuby, JRuby, TruffleRuby
  4. no external gem dependencies
  5. Concurrent.available_processor_count

basics

~20 s

Core Ruby ships low-level primitives: Thread, Mutex, Queue, ConditionVariable, Monitor. The concurrent-ruby gem adds tested higher-level tools (thread pools, Concurrent::Promises futures, Concurrent::Map, atomic variables) with the same guarantees on every major Ruby implementation, so libraries build on it instead of hand-rolling them.

solid answer

~30 s

Core Ruby gives you the building blocks: `Thread`, `Mutex`, `Thread::Queue`/`SizedQueue`, `ConditionVariable` and `Monitor`. It has no thread pool, no composable future, no concurrent hash and no atomic counter. The concurrent-ruby gem (`gem "concurrent-ruby"`, `require "concurrent"`) adds executors such as `Concurrent::FixedThreadPool`, `Concurrent::Promises.future`, `Concurrent::Map`, `Concurrent::AtomicFixnum` and `AtomicReference`, latches, semaphores and timers. Frameworks and servers depend on it because its abstractions are documented as thread-safe on CRuby, JRuby and TruffleRuby alike, it has no external gem dependencies, and it saves every library from re-implementing and re-debugging the same pool or cache. Puma, for example, needs it for `workers :auto`, which reads `Concurrent.available_processor_count`.

go deeper

for a junior

Know the name: concurrent-ruby, loaded with require "concurrent". Recall that it adds thread pools, futures, a concurrent map and atomic counters that core Ruby lacks.

for a middle

Explain which core primitive each gem abstraction replaces, and why the GVL does not make compound operations atomic.

for a senior

Justify a dependency on the gem in shared code: portable guarantees across Ruby implementations, zero transitive dependencies, and fewer hand-rolled pools to debug.

for a principal

Set a team rule for concurrency code: when core primitives suffice, when the gem is required, and which abstractions are allowed in shared libraries.

## What core Ruby gives you Ruby's core concurrency API is deliberately small. Out of the box you get: - **`Thread`**: start work concurrently, `join` it, read its `value`. - **`Mutex`**: mutual exclusion with `synchronize`. - **`Thread::Queue` and `Thread::SizedQueue`**: blocking FIFO queues for handing work between threads. - **`ConditionVariable`** and **`Monitor`**: waiting for a condition and reentrant locking. Everything above that level (a pool of reusable worker threads, a result you can chain, a hash many threads can update safely, a counter that increments atomically) you would have to build yourself from these pieces. ## What concurrent-ruby adds The gem is published as `concurrent-ruby` and loaded with `require "concurrent"`. Its main groups: | Group | Examples | Replaces hand-rolled | |---|---|---| | Executors | `Concurrent::FixedThreadPool`, `CachedThreadPool`, `ThreadPoolExecutor` | a Queue plus N looping threads | | Futures | `Concurrent::Promises.future`, `zip`, `then`, `rescue` | threads that store results in shared variables | | Collections | `Concurrent::Map` | a Hash wrapped in a Mutex | | Atomics | `Concurrent::AtomicFixnum`, `AtomicBoolean`, `AtomicReference` | a Mutex around one variable | | Coordination | `Concurrent::CountDownLatch`, `Semaphore`, `Event` | ConditionVariable loops | | Scheduling | `Concurrent::TimerTask`, `ScheduledTask` | sleeping threads | It also exposes helpers such as `Concurrent.processor_count` and `Concurrent.available_processor_count`, the latter taking a container's CPU quota into account. ## Why frameworks depend on it A web framework, a job library or an application server cannot know which Ruby it will run on, and it cannot afford subtle concurrency bugs in shared infrastructure. The gem addresses that in four ways: 1. **Portable guarantees.** The gem documents every abstraction as thread-safe on CRuby, JRuby and TruffleRuby. The GVL on CRuby hides some races that surface on implementations without one, so code built on these abstractions behaves the same everywhere. 2. **No dependencies of its own.** One of the gem's stated design goals is to stay free of external gem dependencies, so adding it to a dependency tree is cheap. 3. **Shared, tested infrastructure.** A pool with a bounded queue, a rejection policy and orderly shutdown is easy to get subtly wrong. Libraries reuse one implementation instead of each shipping its own. 4. **Utility functions.** Puma's `workers :auto` (and `WEB_CONCURRENCY=auto`) calls `Concurrent.available_processor_count` and fails with a message asking you to add the gem if it is missing. ## What it does not do - It does **not** remove the GVL: on CRuby, threads in a pool still interleave for CPU-bound Ruby code; pools pay off when tasks wait on I/O. - It does **not** make your own objects thread-safe. The README states plainly that no library can stop you from sharing a mutable object between threads and modifying it on both. - Its older classes, `Concurrent::Future` and `Concurrent::Promise`, still exist, but `Concurrent::Promises` is the newer, unified API that the README describes as combining their features with better performance. ## Adding it to a project The gem is usually already in your `Gemfile.lock` as a transitive dependency, but code that calls it directly should declare it: ```ruby # Gemfile gem "concurrent-ruby" gem "concurrent-ruby-ext" # optional C extensions for CRuby ``` Two related gems exist: - **`concurrent-ruby-ext`** holds optional C extensions that speed up some classes (for example the atomics) on CRuby; it is always released with the same version as the main gem, and the pure-Ruby fallbacks keep the same API when it is absent. - **`concurrent-ruby-edge`** carries experimental abstractions; it stays at 0.y.z, and the project documents that a minor version bump there may break compatibility, so it is not for production dependencies. ## When to reach for it Use core primitives for a single background thread or a simple producer/consumer queue. Reach for concurrent-ruby when you need a bounded worker pool, futures you can combine, a shared cache with atomic compute-if-absent, or counters and flags updated from many threads, and when the code must run on more than one Ruby implementation.

  • If CRuby has a GVL, why would a library still use `Concurrent::Map` or `AtomicFixnum` instead of a plain Hash or Integer?
    The GVL does not make compound operations atomic: a check-then-set on a Hash or `count += 1` can still interleave between threads. The gem's abstractions give explicit atomic operations, and they keep their guarantees on JRuby and TruffleRuby, where there is no GVL at all. A library that must work everywhere cannot rely on CRuby's lock.
  • Why does Puma ask you to add concurrent-ruby to your Gemfile when you set `workers :auto`?
    `workers :auto` sets the number of worker processes to `Concurrent.available_processor_count`, which comes from the gem and accounts for a container CPU quota. Puma requires the gem's processor counter only in that case, and raises with a message asking you to add `concurrent-ruby` when it cannot load it. Setting an explicit worker count avoids the dependency.

saying these in an interview costs you the question

  • Thinks core Ruby ships a thread pool class
  • Says concurrent-ruby removes the GVL so pools run Ruby in parallel
  • Believes the gem makes any object shared between threads safe
  • Reaches for Concurrent::Future as the current API instead of Concurrent::Promises
  • Claims the GVL makes check-then-set on a Hash atomic