Stateful Goroutines in Clojure

Our example demonstrates how to manage state using Clojure’s built-in concurrency primitives. This approach aligns with Clojure’s philosophy of managing shared state through controlled mutation.

(ns stateful-agents
  (:require [clojure.core.async :as async]))

(defn create-read-op [key]
  {:key key
   :resp (async/chan)})

(defn create-write-op [key val]
  {:key key
   :val val
   :resp (async/chan)})

(defn state-agent []
  (let [state (atom {})
        reads (async/chan)
        writes (async/chan)]
    (async/go-loop []
      (async/alt!
        reads ([read]
          (let [value (get @state (:key read))]
            (async/>! (:resp read) value))
          (recur))
        writes ([write]
          (swap! state assoc (:key write) (:val write))
          (async/>! (:resp write) true)
          (recur))))
    {:reads reads :writes writes}))

(defn read-routine [reads read-ops]
  (async/go-loop []
    (let [read (create-read-op (rand-int 5))]
      (async/>! reads read)
      (async/<! (:resp read))
      (swap! read-ops inc)
      (Thread/sleep 1)
      (recur))))

(defn write-routine [writes write-ops]
  (async/go-loop []
    (let [write (create-write-op (rand-int 5) (rand-int 100))]
      (async/>! writes write)
      (async/<! (:resp write))
      (swap! write-ops inc)
      (Thread/sleep 1)
      (recur))))

(defn -main []
  (let [read-ops (atom 0)
        write-ops (atom 0)
        {:keys [reads writes]} (state-agent)]
    
    (dotimes [_ 100]
      (read-routine reads read-ops))
    
    (dotimes [_ 10]
      (write-routine writes write-ops))
    
    (Thread/sleep 1000)
    
    (println "readOps:" @read-ops)
    (println "writeOps:" @write-ops)))

In this Clojure version, we use core.async channels to manage communication between different parts of our program, similar to how Go uses channels. The state-agent function creates a process that manages a shared state, responding to read and write requests.

We create 100 read routines and 10 write routines, each running in its own go-loop (Clojure’s equivalent to goroutines). These routines send read and write operations to the state agent and update atomic counters for the number of operations performed.

To run the program, you would typically put this code in a file named stateful_agents.clj and use the Clojure CLI or Leiningen to run it:

$ clj -M stateful_agents.clj
readOps: 71708
writeOps: 7177

This Clojure-based state management example completes about 80,000 total operations, similar to the original Go version.

While this approach might seem more complex than using a simple atom or ref, it can be useful in scenarios where you need fine-grained control over state access or when dealing with multiple interdependent state changes. As with any concurrency model, use the approach that makes your program’s behavior easiest to reason about and verify for correctness.