Cross-platform concurrency primitives for Clojure and ClojureScript.
All primitives expose a unified macro API that compiles to lock-free atomics on the JVM and direct mutation on ClojureScript. The API and semantics are identical across platforms, with the only difference being that JVM operations are thread-safe while ClojureScript operations are single-threaded.
;; deps.edn
co.multiply/conc {:mvn/version "0.1.3"}co.multiply.conc.deque — a lock-free doubly-linked deque with eager folding.
- O(1) add to tail, O(1) removal by reference, O(1) poll from head
- Links track their state: pending, live (in a deque), or removed
- A link can only be in one deque at a time and cannot be re-added after removal
- Eager folding on removal splices out removed nodes, keeping traversal fast
- Implements
Counted(both platforms),Iterable(JVM),ISeqable/IIterable(CLJS)
(require '[co.multiply.conc.deque :as d])
(let [dq (d/deque)
a (my-link)
b (my-link)]
(d/add dq a)
(d/add dq b)
(d/size dq) ;=> 2
(d/remove dq a)
(d/poll dq) ;=> b
(d/isEmpty dq)) ;=> trueThe forEach macro provides a convenient way to walk all live links:
(d/forEach [link dq]
(println link))co.multiply.conc.queue — a concurrent queue backed by the Deque, with O(1) removal.
- Values are wrapped internally; the queue API deals in values, not links
addreturns a link that can be used for O(1) removal viaremovepollreturns the first value (FIFO order), or nil if empty- Null/nil values are not permitted (poll uses nil to indicate empty)
- Implements
Counted,Iterable/ISeqable/IIterable
(require '[co.multiply.conc.queue :as q])
(let [queue (q/queue)
link (q/add queue :task-1)]
(q/add queue :task-2)
(q/add queue :task-3)
(q/poll queue) ;=> :task-1
(q/remove link) ;=> false (already polled)
(vec queue)) ;=> [:task-2 :task-3]The returned link enables cancellation-style patterns — enqueue a task, then remove it before it's processed if it's no longer needed:
(let [queue (q/queue)
link (q/add queue :expensive-task)]
;; ... conditions change ...
(q/remove link) ;=> true (removed before poll)
(q/poll queue)) ;=> nilco.multiply.conc.set — a concurrent set backed by ConcurrentHashMap (JVM) or a Clojure set (CLJS).
- O(1) add, remove, and contains operations
- Implements
java.util.Seton the JVM for full collections interop - Implements
Counted,Iterable/ISeqable/IIterable
(require '[co.multiply.conc.set :as s])
(let [set (s/set)]
(s/add set :a) ;=> true
(s/add set :b) ;=> true
(s/add set :a) ;=> false (already present)
(s/contains? set :a) ;=> true
(s/remove set :a) ;=> true
(s/size set) ;=> 1
(vec set)) ;=> [:b]co.multiply.conc.integer — a cross-platform atomic integer.
- Backed by
AtomicIntegeron the JVM, mutable integer on ClojureScript - Supports increment, decrement, get, compareAndSet, and update-with-function
- All operations return
intvalues (not boxed)
(require '[co.multiply.conc.integer :as i])
(let [counter (i/integer 0)]
(i/incrementAndGet counter) ;=> 1
(i/getAndIncrement counter) ;=> 1 (returns old value)
(i/getValue counter) ;=> 2
(i/updateAndGet counter #(* % 10)) ;=> 20
(i/compareAndSet counter 20 0)) ;=> trueOn the JVM, all primitives are thread-safe. Deque and Queue use VarHandle CAS operations for lock-free concurrency. Set is backed by ConcurrentHashMap. Integer is backed by AtomicInteger.
On ClojureScript, the same API is available but operations use direct field mutation (single-threaded). This provides the same algorithmic complexity without the overhead of atomic operations.
Eclipse Public License 2.0. Copyright (c) 2025 Multiply. See LICENSE.