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.