Atomic Counters in Crystal

Our primary mechanism for managing state in Crystal is communication over channels. However, there are a few other options for managing state. Here we’ll look at using atomic operations for atomic counters accessed by multiple fibers.

require "atomic"

def main
  # We'll use an atomic integer to represent our (always-positive) counter.
  ops = Atomic(UInt64).new(0)

  # We'll use a channel to wait for all fibers to finish their work.
  done = Channel(Nil).new

  # We'll start 50 fibers that each increment the counter exactly 1000 times.
  50.times do
    spawn do
      1000.times do
        # To atomically increment the counter we use add.
        ops.add(1)
      end
      done.send(nil)
    end
  end

  # Wait until all the fibers are done.
  50.times { done.receive }

  # Here no fibers are writing to 'ops', but using get
  # it's safe to atomically read a value even while
  # other fibers are (atomically) updating it.
  puts "ops: #{ops.get}"
end

main

We expect to get exactly 50,000 operations. Had we used a non-atomic integer and incremented it with ops += 1, we’d likely get a different number, changing between runs, because the fibers would interfere with each other.

$ crystal run atomic_counters.cr
ops: 50000

In Crystal, we use Atomic types from the atomic module to achieve atomic operations. The spawn keyword is used to create fibers (lightweight threads) which are similar to goroutines in Go. Instead of using a WaitGroup, we use a Channel to synchronize the completion of fibers.

The Atomic class provides methods like add and get for atomic operations, which are similar to the Add and Load methods in Go’s atomic package.

Next, we’ll look at mutexes, another tool for managing state.