5 Home
dxtr edited this page 2026-10-17 04:34:44 +02:00

cl-threads

Threads and a job system for Common Lisp on SBCL. It gives you a pool of worker threads with a parallel for, job groups you can start and check on later, and storage for each worker, built on threads, locks and semaphores you can use on their own as well.

It is the job system of the Pyssel engine, but depends on nothing else and can be used on its own.

(asdf:load-system :cl-threads)

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:th #:cl-threads)))
(in-package #:example)

(defvar *numbers* (make-array 1000000 :element-type 'double-float :initial-element 0.5d0))

(th:with-job-pool (pool)
  ;; A running total for each worker, so they never share one.
  (let ((totals (th:make-worker-local pool (lambda (index)
                                             (declare (ignore index))
                                             (make-array 1 :element-type 'double-float)))))
    (th:parallel-for pool 0 (length *numbers*)
                     (lambda (start end)
                       (let ((total (th:worker-local-value totals)))
                         (loop for index from start below end
                               do (incf (aref total 0) (aref *numbers* index))))))
    (reduce #'+ (th:worker-local-values totals) :key (lambda (total) (aref total 0)))))
;; => 500000.0d0

What it gives you

  • parallel-for splits a range of integers into chunks and runs them on the workers, then returns. Once the pool is warmed up it allocates nothing, so it can run every frame.
  • Job groups count jobs submitted together. Submitting returns at once; you can check whether the jobs are done without waiting, wait for them, or never look at them again. This suits loading assets, rasterising glyphs or compiling shaders in the background.
  • Nesting without deadlock. A worker that waits runs the queued jobs of what it waits for, so jobs may use parallel-for themselves, even on a pool of one worker, and wait for any group, as long as no job waits for itself.
  • Errors you can see. A job's error comes back to whoever waits, or goes to a handler for jobs nobody waits for, or opens the debugger in the worker with retry-job and skip-job restarts.
  • Worker storage. A value per worker, such as a scratch buffer or a command pool, so workers never contend for one.
  • Threads and semaphores. Threads that can be joined any number of times from any number of threads and return their function's values, that know themselves from their first instruction, take bindings of their own, and can be interrupted to look at them in the debugger, and semaphores with counts and timeouts. The interface follows Bordeaux-threads where that is what you would expect.

Pages

Page What it covers
Getting Started Loading, a first pool, running the tests
Job Pools Making and destroying pools, how many workers, what workers see of special variables and output
Parallel For Splitting work across workers, chunk sizes, nesting, running every frame without allocating
Job Groups Submitting jobs and carrying on, checking and waiting, reusing groups
Errors in Jobs Where a job's error goes, error handlers, job-error, debugging with restarts
Worker Storage A value for each worker
Threads and Synchronisation Threads, joining, bindings, interrupting, locks and semaphores
Recipes Patterns for a game engine: per-frame work, background loading, queues between threads
Thread Rules What may be shared between threads, what a job may do, and what cl-threads uses of SBCL underneath
API Reference Every exported function, macro, type and condition