Compatibility¶
- Implementation: SBCL only. Tested against SBCL 2.6.0.
- Dependencies: none at runtime.
cl-concurrent-kit/testdepends on cl-weave v1.1.4, test-only -- its test DSL (describe/it/expect/signals), property-based testing and fuzzing (it-property,it-fuzz,gen-integer,gen-list,gen-boolean), soft assertions (with-soft-assertions, reporting every failingexpectin anitblock instead of stopping at the first), and benchmark facility (benchmark, used bybenchmarks/run-benchmarks.lisp) are all in active use, not just the assertion macros. (v1.1.2 skipped deliberately: its tag exists butrelease.ymlnever actually published it -- unrelated to a real, SBCL-only-affecting fix in v1.1.3 that removed an unconditionalsb-coverdependency from cl-weave's main system -- so v1.1.4, which additionally guards several ECL portability paths this SBCL-only project never exercises, is the version actually pinned.) That same script additionally depends on cl-cli v1.2.0, benchmark-tooling-only, for its--only/scale argument parsing -- neither it norcl-weaveis reachable fromcl-concurrent-kit's own:depends-on (), only fromflake.nix'sCL_SOURCE_REGISTRYfor the script and test system respectively.flake.nixitself is built with cl-nix-forge, the org's Nix packaging library -- a build-time-only, Nix-level dependency with no Lisp component. A fourth nerima-lisp tool, paredit-cli, is not a dependency at all in the ASDF/Nix sense -- it never appears in:depends-on,flake.nix, orCL_SOURCE_REGISTRY-- but is the structure-aware refactoring tool this project's own source history was edited with throughout: renames (refactor rename-function), balance validation after every structural edit (inspect check), and definition discovery (inspect definitions) all ran through it rather than by hand. - Platforms:
x86_64-linuxis the only platform CI actually gates.aarch64-darwinis also declared, fornix develop/nix buildon the development machine -- dropped briefly on 2026-08-01 for carrying no CI gate, then re-declared on 2026-08-02 once the org's own PACKAGE_STANDARD.md accepted that trade-off explicitly rather than requiring every declared system to be CI-gated.aarch64-linuxandx86_64-darwinare nobody's verification and stay undeclared. Seeflake.nix.
cl-concurrent-kit wraps sb-thread and sb-ext directly rather than
depending on bordeaux-threads; see Architecture for why.
Porting to another implementation would mean reimplementing
src/primitives.lisp against that implementation's native thread API --
everything above that layer (promise, channel, select, executor,
scope) is portable Common Lisp with no sb-* references.
Stability¶
The public API is exactly src/package.lisp's :export list -- nothing
reached only through a package-qualified cl-concurrent-kit:: symbol is
covered by semantic versioning. Every exported symbol is exercised by at
least one test in t/, and nix flake check (tests, docs, formatting,
coverage) gates every merge to main and every tagged release; see
.github/workflows/ci.yml
and release.yml.
flake.lock pins cl-weave and cl-nix-forge to specific tagged releases
(bumped by hand when this package adopts a new one) and nixpkgs/treefmt-nix
to a commit refreshed automatically by
flake-update.yml's
weekly cron, each update going through the same nix flake check gate as any
other change before merging.
Production readiness¶
This is a library, loaded into a caller's own SBCL image -- there is no
service to deploy and no SLA to publish; what a caller integrating it needs
to know is below. (For round-trip overhead per primitive, run
benchmarks/run-benchmarks.lisp, described in the repository's own
top-level README.)
- Error handling: every blocking operation that accepts
:timeoutsignalsoperation-timed-out(never returns a sentinel value) on expiry; every other failure mode is its own condition (promise-already-fulfilled,channel-closed,task-cancelled,scope-error,latch-count-underflow,barrier-broken,promise-cancelled,promise-empty-input,promise-all-failed,executor-queue-full) subclassingcl-concurrent-kit-error, so a caller can catch that one base condition to handle any failure this library signals without enumerating each one. - Thread safety: every public struct (
promise,channel,executor,task-scope,countdown-latch,barrier) owns its own lock and is safe to share across threads through its documented operations only; none of them is safe to mutate through slot accessors directly (all writer accessors are internal,%-prefixed). - Resource cleanup:
make-executorstarts worker threads that outlive the call untilshutdown-executoris called -- there is no finalizer, by design, matchingsb-thread's own contract; a long-running process that creates executors without shutting them down leaks threads exactly as it would leak any other unclosed resource.with-task-scopeandfuturehave no equivalent leak: every thread either one starts is guaranteed to have been joined (structured concurrency) or was never blocked on externally (future's own thread exits on its own). - Known limitation: cancellation (
check-cancelled,with-task-scope) is cooperative, not preemptive -- see Architecture for why forcing it would cost the guarantee a scope exists to make. Where a body really must be bounded whatever it is doing,with-timeoutis the preemptive escape hatch (it interrupts the thread outright), with the asynchronous-unwind caveat that comes with one: Architecture. - Scope: single SBCL image only. Nothing here coordinates across OS
processes or machines;
promise/channel/executor/task-scopeobjects are not serializable and sharing one across images is not a supported use. - Timeout audit: every place this project itself runs a command or blocks on a result has an explicit bound, checked directly rather than assumed:
flake.nix's three shell invocations (checks.coverage-lcov,checks.benchmark,apps.benchmark) each wrap theirsbclcall intimeout --signal=KILL <N>s.- Every job in every workflow under
.github/workflows/(ci.yml,docs.yml's two jobs,flake-update.yml,release.yml) declares its owntimeout-minutes:-- there is no job relying on GitHub's default. - The test suite has a global backstop (
run-all :timeout-ms 20000int/package.lisp) plus its own per-call:timeoutat almost every individual blockingrecv/await. The four bare calls without one (t/channel-test.lisp:102,106,t/promise-test.lisp:231,t/select-test.lisp:241) were each checked individually rather than assumed safe: in every case the value is already available -- sent, cancelled, or buffered -- before the blocking call runs, so it resolves synchronously and structurally cannot block.