AGENTS.md
June 24, 2026 · View on GitHub
Working notes for agents modifying this repository. For the design read
ARCHITECTURE.md; for usage read README.md. This file covers the repo layout,
how to build and test, the invariants you must not break, and the traps that are
easy to fall into.
Repo map
threadpool.hh The pool: ThreadPool, ThreadPoolThread, PackagedTask, TaskWrapper, TaskQueue. Header.
thread.hh Thread<Impl,policy> CRTP base, ThreadPolicyType enum, thread-name free function decls. Header.
thread.cc The only .cc: run_thread/setup_thread/set_thread_name/get_thread_name/sched_getcpu.
blocking_concurrent_queue.h Mutex + condvar queue the pool waits on. Header.
concurrent_queue.h Mutex + std::deque queue (NOT lock-free). Header.
likely.h likely/unlikely macros; self-contained (no config.h).
threadpool_trace.h No-op L_EXC + THREADPOOL_THREAD_REGISTER hook (stands in for Xapiand's log.h/traceback.h).
config.h.in CMake template for pthread feature macros; configured into build/config.h.
test/test.cc Runnable smoke test: pool of N tasks, async futures, TaskQueue.
CMakeLists.txt STATIC library target `threadpool` (+ alias threadpool::threadpool) + CTest test `threadpool`.
LICENSE MIT, Copyright (c) 2015-2019 Dubalu LLC.
README.md What it is, install, usage, API reference, tracing.
ARCHITECTURE.md Internal design, concurrency model, trade-offs.
Only thread.cc is compiled into the library; everything else is header-only.
config.h is generated by CMake into the build directory, not checked in.
Build and run the test
cmake -B build && cmake --build build && ctest --test-dir build
Or directly (you must compile thread.cc too, it is not header-only):
c++ -std=c++20 -I. test/test.cc thread.cc -o test/test && ./test/test
Expected output ends with all threadpool tests passed, exit 0. The CMake
threadpool target is a STATIC library that requests cxx_std_20, links
Threads::Threads, and exposes both the source dir and the generated-config dir
as PUBLIC includes. The test target is threadpool_test; the registered CTest
name is threadpool.
Conventions
- C++20. Required by
std::format(worker naming inthreadpool.hh). Don't drop below it. - No external dependencies. The only includes are the C++ standard library,
pthreads, and the bundled headers. Do not add Xapiand headers (
log.h,strings.hh,traceback.h) back; that is exactly what the trace hooks andstd::formatreplaced. - Tracing and thread registration flow through two injectable hooks, both
no-ops by default:
L_EXC(...)andTHREADPOOL_THREAD_REGISTER(pthread, name). Defaults live inthreadpool_trace.h, each#ifndef-guarded. Boththreadpool.hhandthread.ccreach them through#ifdef THREADPOOL_TRACE_HEADER(include the consumer's header)#else(includethreadpool_trace.h). A consumer injects its own via-DTHREADPOOL_TRACE_HEADER='"my_trace.h"', or defines the macros first. Keep any new trace/registration call behind these macros; never assume they do anything. config.his for pthread feature macros only (HAVE_PTHREADS,HAVE_PTHREAD_SETNAME_NP,HAVE_PTHREAD_SET_NAME_NP,HAVE_PTHREAD_NP_H), generated by CMake fromconfig.h.in.likely.his deliberately self-contained and does NOT includeconfig.h— it detects__builtin_expectwith__has_builtin. Don't reintroduce aconfig.hdependency inlikely.h.- Tabs for indentation, double quotes in code, no em dashes in prose.
Load-bearing invariants
ThreadPolicyTypeand the policy template parameter must stay. The enum and theThreadPolicyType thread_policyparameter onThread,ThreadPool, andThreadPoolThreadare kept for source-compatibility: Xapiand has 25+ call sites of the formThreadPool<T, ThreadPolicyType::x>. The policy is currently runtime-ignored (run_thread/setup_threadtake it unnamed), but deleting it breaks every one of those call sites. If you wire it up to do something (affinity, priority), that is a real behavior change to call out — do it insiderun_thread/setup_threadso call sites stay unchanged.PackagedTask's copy constructor must never actually run. It exists only to satisfystd::function's copyability requirement andassert(false)s (threadpool.hh). The contract is that the task is moved, never copied.- Worker shutdown is two-phase.
end()enqueues onenullptrper thread and lets workers drain first;finish()sets_finishedso workers stop as soon as possible. The worker loop checks_finished(acquire) each iteration and_endingonly when it dequeues nothing. Keepend/finish/the worker loop in sync, or shutdown either hangs or drops queued tasks. - Counters are the only state queries.
_enqueued/_running/_workersare atomics maintained around enqueue/dequeue/run.size()etc. read them with relaxed loads. Keep every fetch_add paired with its fetch_sub on the matching path (enqueue failure rolls back, dequeue decrements_enqueued, run brackets_running), or the reported numbers drift. - The blocking queue is mutex-backed, not lock-free.
ConcurrentQueueis astd::dequeunder astd::mutex;BlockingConcurrentQueueadds a condvar. The class names match moodycamel's lock-free API but the implementation does not. Do not assume lock-free semantics anywhere.
How to extend
- Add a pool state query / control. Follow the existing atomic-counter pattern; read with relaxed loads, never take a lock for a query.
- Wire up a thread policy. Read
thread_policyinsiderun_thread/setup_thread(thread.cc) — e.g. set affinity or priority per policy. Call sites and the template signatures stay exactly as they are. - Plug in tracing / crash registration. Inject a trace header with
-DTHREADPOOL_TRACE_HEADER='"my_trace.h"'that definesL_EXCandTHREADPOOL_THREAD_REGISTER, or define them before including the headers. To recover Xapiand's behavior, mapTHREADPOOL_THREAD_REGISTERtoinit_thread_infoandL_EXCtolog.h's macro. - Always extend the smoke test.
test/test.ccis the only executable check. Any behavioral change should grow a corresponding assertion there.
Traps
- Don't delete
ThreadPolicyTypethinking it is dead. It is unused at runtime but load-bearing at compile time for the consumer's call sites. - Don't reintroduce
config.hintolikely.h. It is self-contained on purpose so the headers work without the CMake-generated config. - Don't add Xapiand headers back.
log.h,strings.hh,traceback.hwere the whole point of the extraction. Route through the hooks andstd::format. TaskQueue::clear()usesConcurrentQueueDefaultTraits::BLOCK_SIZE. The Xapiand original referencedQueue::BLOCK_SIZE, which thisConcurrentQueuedoes not define; it compiled only becauseclear()was never instantiated. The fix here points at the trait that actually defines it. If you re-sync from Xapiand, keep this fix.- Compile
thread.cc. It is the one translation unit; forgetting it gives link errors forset_thread_name/run_thread/sched_getcpu. sched_getcpuis macOS-only here. On Linux glibc already provides it; the bundled definition is guarded by#ifdef __APPLE__. On Apple Silicon it returns-1(no user-space CPU id); callers must treat a negative result as "unknown".
Standalone vs. Xapiand
This is a standalone extraction from Xapiand. The delta from the original is pure decoupling:
threadpool.hhdropped#include "log.h"and#include "strings.hh". The twoL_EXC(...)sites now resolve throughthreadpool_trace.h(no-op by default), andstrings::format(pool->_format, idx)becamestd::vformat(pool->_format, std::make_format_args(idx))(the format strings like"CH{:02}"are alreadystd::formatsyntax).thread.ccdropped#include "traceback.h"; theinit_thread_info(...)call becameTHREADPOOL_THREAD_REGISTER(...)(no-op by default). The hardcoded"Xapiand:"name prefix became theTHREADPOOL_THREAD_NAME_PREFIXmacro (empty by default).likely.hdropped#include "config.h"and detects__builtin_expectdirectly;config.his now CMake-generated and carries only the pthread thread-naming feature macros thatthread.ccneeds.
The pool logic is otherwise unchanged. Keep extraction hygiene separate from
behavior changes so they can be reconciled with upstream. To restore Xapiand's
tracing/registration without editing these files, see "Tracing and thread
registration" in README.md.