Skip to content
Concurrency with bordeaux-threads
step 1/5

Reading — step 1 of 5

Learn

~2 min readOptimization, Concurrency, Idioms

Common Lisp standard doesn't define threads — but every modern implementation has them, and bordeaux-threads is the portable wrapper.

Why CL threads matter

Common Lisp threads are real OS threads — preemptive, parallel on multi-core. Different from Lua coroutines (cooperative) or Python's GIL-bound threads.

Basic thread

(ql:quickload :bordeaux-threads)

(let ((thread (bt:make-thread
                  (lambda () (format t "hello from thread~%")))))
    (bt:join-thread thread))

Mutexes

(defparameter *lock* (bt:make-lock))
(defparameter *count* 0)

(bt:with-lock-held (*lock*)
    (incf *count*))

with-lock-held ensures the lock is released even on error.

Condition variables

(defparameter *cv* (bt:make-condition-variable))
(defparameter *queue* '())
(defparameter *queue-lock* (bt:make-lock))

;; Producer:
(bt:with-lock-held (*queue-lock*)
    (push item *queue*)
    (bt:condition-notify *cv*))

;; Consumer:
(bt:with-lock-held (*queue-lock*)
    (loop while (null *queue*)
          do (bt:condition-wait *cv* *queue-lock*))
    (pop *queue*))

Atomic operations

SBCL has its own atomics module:

(sb-ext:atomic-incf *counter*)
(sb-ext:atomic-update *cell* (lambda (old) (* old 2)))

Channels

For higher-level concurrency, use chanl (CSP-style) or lparallel (parallel map/reduce):

(ql:quickload :lparallel)
(setf lparallel:*kernel* (lparallel:make-kernel 4))

(lparallel:pmapcar (lambda (n) (* n n)) (loop for i from 1 to 1000 collect i))

pmapcar — parallel mapcar across CPUs. Good for embarrassingly parallel work.

Special variables and threads

Each thread has its own dynamic binding stack. So (let ((*log-level* :error)) ...) only affects the thread that ran the let. Useful for thread-local config.

Race conditions in Lisp

Lisp has the same hazards as any language with shared mutable state. Be careful with:

  • Hash tables — use a lock or :synchronized t (SBCL)
  • Lists — cons, setf aren't atomic across operations
  • Special variables in let — fine within one thread; cross-thread, use atomics

Practical advice

  • Default to single-threaded; add threads when profiling shows CPU underutilization
  • Lparallel for compute-bound parallel work
  • Bordeaux for explicit thread management (servers, GUIs)
  • Avoid sharing state — pass data through queues/channels

We can't run threads in the Judge0 sandbox (single-threaded execution), but the patterns above are how real CL apps use multi-core.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…