Skip to content

multiplyco/conc

Repository files navigation

Conc

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.

Installation

;; deps.edn
co.multiply/conc {:mvn/version "0.1.3"}

Deque

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))  ;=> true

The forEach macro provides a convenient way to walk all live links:

(d/forEach [link dq]
  (println link))

Queue

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
  • add returns a link that can be used for O(1) removal via remove
  • poll returns 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))   ;=> nil

Set

co.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.Set on 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]

Integer

co.multiply.conc.integer — a cross-platform atomic integer.

  • Backed by AtomicInteger on the JVM, mutable integer on ClojureScript
  • Supports increment, decrement, get, compareAndSet, and update-with-function
  • All operations return int values (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))          ;=> true

Platform differences

On 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.

License

Eclipse Public License 2.0. Copyright (c) 2025 Multiply. See LICENSE.

About

Concurrency primitives

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors