Skip to content

Instantly share code, notes, and snippets.

@ghadishayban
Last active November 21, 2025 16:23
Show Gist options
  • Select an option

  • Save ghadishayban/de1b9334b7fcedb15a42915c72f59715 to your computer and use it in GitHub Desktop.

Select an option

Save ghadishayban/de1b9334b7fcedb15a42915c72f59715 to your computer and use it in GitHub Desktop.

Data race in 1.12 LazySeq 11/18/2025

  • Joe Lane (Minnesota)
  • Francis Avila (Louisiana)
  • Ghadi Shayban (South Carolina)

In the 1.12 LazySeq ReentrantLocks reimpl, there is a significant data race that results in an incorrect nil result from concurrent calls to seq().

Clone then clj -X:repro to provoke the race (~10s on my M1), or clj -X:repro:accelerate (<100ms)

The data race is that seq() checks for the lock's absence as a signal that s is published. But, neither s nor the lock are volatile, so there is no ordering between their publishing, and an observer can see these two statements reordered: nil lock but no s yet. The only happens-before edge seq() is locking and unlocking, and seq() can ignore the lock entirely.

{:paths ["."]
:deps {org.clojure/clojure {:mvn/version "1.12.3"}}
:aliases {:repro {:exec-fn repro/main}
:accelerate {:jvm-opts ["-XX:-UseCompressedOops"]}
;; < 1.12 has a different, correct impl of LazySeqs using monitors, not RLocks
:monitors {:deps {org.clojure/clojure {:mvn/version "1.11.4"}}}}}
(ns repro)
;; Joe Lane (Minnesota), Francis Avila (Louisiana) and Ghadi Shayban (South Carolina) 11/18/2025
(defn concurrently-realize
"attempts to realize the same non-empty lazy-seq by calling seq from
multiple threads. returns all results iff any thread doesn't return
a seq"
[threads]
(let [start (java.util.concurrent.CountDownLatch. 1)
s (lazy-seq (list 42))
f #(do (.await start) (seq s))
futs (mapv future-call (repeat threads f))]
(.countDown start)
(let [rets (mapv deref futs)]
(when-not (every? seq? rets)
rets))))
(defn run
[& {:keys [threads max-iters]
:or {threads 4
max-iters 500000}}]
(let [begin (System/currentTimeMillis)]
(loop [n 0]
(if-let [result (or (concurrently-realize threads)
(and (>= n max-iters) :no-repro))]
{:iters n
:result result
:ms (- (System/currentTimeMillis) begin)}
(recur (inc n))))))
(defn main
[args]
(prn (run args)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment