Atomic Counters in Clojure

In Clojure, we can use atoms to manage shared state across multiple threads. This is similar to atomic counters in other languages. Let’s look at how we can implement an atomic counter accessed by multiple threads.

(ns atomic-counters
  (:require [clojure.core.async :as async]))

(defn main []
  ; We'll use an atom to represent our (always-positive) counter.
  (let [ops (atom 0)
        ; We'll use a countdown latch to wait for all threads to finish.
        latch (java.util.concurrent.CountDownLatch. 50)]
    
    ; We'll start 50 threads that each increment the counter exactly 1000 times.
    (dotimes [_ 50]
      (async/thread
        (dotimes [_ 1000]
          ; To atomically increment the counter we use swap!
          (swap! ops inc))
        (.countDown latch)))
    
    ; Wait until all the threads are done.
    (.await latch)
    
    ; Here no threads are writing to 'ops', but using deref it's safe 
    ; to atomically read a value even while other threads might be updating it.
    (println "ops:" @ops)))

(main)

In this Clojure version:

  1. We use an atom instead of atomic.Uint64. Atoms in Clojure provide atomic updates to a single reference.

  2. Instead of a WaitGroup, we use a CountDownLatch from Java’s concurrent utilities. This serves a similar purpose of waiting for all threads to complete.

  3. We use async/thread to create threads, which is similar to goroutines in Go.

  4. The swap! function is used to atomically update the atom. It’s similar to the Add method in Go’s atomic package.

  5. We use @ops (which is shorthand for (deref ops)) to read the final value of the atom, similar to the Load method in Go.

When you run this program, you should see:

$ clj atomic-counters.clj
ops: 50000

We expect to get exactly 50,000 operations. The use of atoms ensures that all updates to the counter are atomic, preventing race conditions that could occur with non-atomic operations.

This example demonstrates how Clojure’s concurrency primitives, particularly atoms, can be used to safely manage state across multiple threads. While the syntax and specific mechanisms differ from Go, the underlying concept of atomic operations for safe concurrent access remains the same.