No results
5
Home
dxtr edited this page 2026-10-17 04:34:44 +02:00
Table of Contents
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-forsplits 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-forthemselves, 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-jobandskip-jobrestarts. - 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 |