skip to content

In concurrent-ruby, when do you use AtomicFixnum versus AtomicReference, and what must an AtomicReference#update block never do?

level: seniorimportance: nice to knowfreq 20%

answer

  1. one Integer vs any object
  2. increment returns the new value
  3. update retries on conflict
  4. compare by identity, not ==
  5. store immutable values only

basics

~20 s

AtomicFixnum holds one Integer with atomic increment, decrement and compare_and_set. AtomicReference holds any object and swaps it atomically. Its update block may run several times, so it must be side-effect free and return a new object rather than mutate the old one.

solid answer

~40 s

`Concurrent::AtomicFixnum.new(0)` is for a single integer shared by threads: `increment(delta = 1)` and `decrement` return the new value, `compare_and_set(expect, update)` returns true or false, and `update { |v| ... }` retries until it wins. `Concurrent::AtomicReference.new(obj)` holds any object: `get`/`set`, `get_and_set` returning the old value, `compare_and_set(old, new)`, and `update { |old| new }`. Two rules make it safe. First, `update` loops `compare_and_set` until no other thread changed the value in between, so the block can run more than once: no sending, logging or counting inside it. Second, the reference only protects the **pointer**: `ref.get << item` mutates a shared Array with no protection at all. Store frozen values and build a new one in the block, like `ref.update { |list| (list + [item]).freeze }`. For non-numeric values `compare_and_set` compares by identity, not `==`.

go deeper

for a junior

Recall that AtomicFixnum gives a thread-safe counter with increment and decrement, and that AtomicReference holds one object you swap as a whole.

for a middle

Explain compare_and_set, what increment and get_and_set return, and why update retries until no other thread interfered.

for a senior

Spot side effects and in-place mutation inside update blocks, and choose frozen value objects so the atomic swap actually protects the state.

for a principal

Weigh optimistic atomics against locks and message passing for shared state, and set conventions that keep shared values immutable.

## Two atomic holders Both classes hold one value that many threads read and change, without an explicit `Mutex` in your code: | | `Concurrent::AtomicFixnum` | `Concurrent::AtomicReference` | |---|---|---| | Holds | one Integer | any object | | Read / write | `value`, `value=` | `get`/`value`, `set`/`value=` | | Arithmetic | `increment(delta = 1)`, `decrement(delta = 1)`, aliases `up`/`down`; both return the new value | none | | Swap | `compare_and_set(expect, update)` | `compare_and_set(old, new)`, `get_and_set(new)` (alias `swap`) returns the old value | | Functional update | `update { \|v\| new }` | `update`, `try_update` (returns `nil` on conflict), `try_update!` (raises `Concurrent::ConcurrentUpdateError`) | On CRuby the gem uses a C implementation when the optional `concurrent-ruby-ext` gem is installed, and a Mutex-based one otherwise; the API is the same. ## AtomicFixnum: counters and gauges For the push-notification service, counting deliveries across pool threads is the textbook case: ```ruby SENT = Concurrent::AtomicFixnum.new(0) FAILED = Concurrent::AtomicFixnum.new(0) PUSH_POOL.post(token) do |t| PushClient.deliver(t, message) SENT.increment rescue => e FAILED.increment end ``` `increment` returns the value after the change, so `SENT.increment % 1_000 == 0` can drive a progress log without a separate read. The initial value must be an Integer; anything else raises `ArgumentError`. ## AtomicReference: swapping whole objects Use `AtomicReference` when the shared state is a structure, such as the current list of muted users or a configuration snapshot: ```ruby MUTED = Concurrent::AtomicReference.new([].freeze) def mute(user_id) MUTED.update { |list| (list + [user_id]).freeze } end def muted?(user_id) = MUTED.get.include?(user_id) ``` Readers call `get` and see either the old list or the new one, never a half-built one. ## How update works, and the two rules it imposes `update` is a loop: read the current value, call your block with it, and try `compare_and_set(old, new)`; if another thread changed the value in the meantime, start again. That design gives two rules: 1. **The block must be free of side effects.** It can run several times for one successful update. Delivering a notification, incrementing a counter or writing a log line inside it can therefore happen more than once. 2. **The block must return a new object instead of mutating the old one.** `MUTED.update { |list| list << user_id }` mutates the shared Array in place, which other threads may be reading, and returns the same object, so the swap protects nothing. The reference makes the pointer atomic, not the object it points to. Frozen values make the mistake raise `FrozenError` instead of racing silently. ## Identity, not equality For non-numeric values `compare_and_set(old, new)` succeeds only when the current value is **the same object** as `old` (`equal?`), not merely `==` to it. Passing a freshly built but equal Array therefore fails. For Numeric values the gem adds a wrapper that compares with `==`, so `compare_and_set(5, 6)` works with any `5`. ## The rest of the atomic family - **`Concurrent::AtomicBoolean`**: a flag with `true?`, `false?`, `make_true` and `make_false`; the `make_` methods return `true` only if the value actually changed, which makes "first thread to flip the flag wins" easy. - **`Concurrent::AtomicMarkableReference`**: a reference paired with a boolean mark, swapped together. - **`Concurrent::AtomicFixnum`** and **`AtomicReference`**: the counter and the general holder covered above. ## When to use something else - If several values must change together, a single `AtomicReference` holding a frozen value object (for example a `Data` instance) still works; separate atomics do not. - If the critical section does I/O or must run exactly once, a `Mutex` is clearer than an optimistic retry loop.

  • What is the difference between `update`, `try_update` and `try_update!` on `Concurrent::AtomicReference`?
    `update` retries its block until `compare_and_set` succeeds and returns the new value. `try_update` makes one attempt and returns `nil` if another thread changed the value first. `try_update!` also makes one attempt but raises `Concurrent::ConcurrentUpdateError` on conflict. Use the `try_` forms when retrying would be wrong or when you want to count contention.
  • Why does `ref.compare_and_set([1, 2], [1, 2, 3])` return false even though `ref.get == [1, 2]`?
    For non-numeric values the swap compares object identity, not equality: the current value must be the very object passed as `old`. A new `[1, 2]` literal is a different object. Read the current value with `get` and pass that same object, or use `update`, which does this for you.

saying these in an interview costs you the question

  • Sends a notification inside an AtomicReference#update block
  • Appends to the stored Array inside update and returns it
  • Expects compare_and_set to match an equal but different object
  • Thinks AtomicFixnum#increment returns the value before the change
  • Believes an AtomicReference makes the stored object's methods thread-safe