A lightweight, dependency-free thread pool library for Free Pascal. It provides an unbounded pool for straightforward parallel work and a bounded pool for producer-consumer workloads that need backpressure.
Quick start · Cheat sheet · API documentation · Examples · v0.9.0 release notes
Tip
✨ New in v0.9.0: observable task handles, task batches, efficient chunked ranges, and pending-work cancellation. Existing v0.8 queueing code remains source-compatible. See the task API.
Note
This library is designed for simple parallel processing and learning-friendly integration. It is not intended to replace high-load, production-scale frameworks such as mORMot2, ezthreads, or OmniThreadLibrary.
ThreadPool.Simple |
ThreadPool.ProducerConsumer |
|
|---|---|---|
| Queue | Dynamically growing FIFO | Fixed-size circular FIFO |
| Capacity | Unbounded | Configurable; 1024 by default |
| Submission | Immediate | Timeout-aware backpressure |
| Convenience | Managed GlobalThreadPool |
Create a pool instance |
| Best for | Predictable, moderate fire-and-forget work | Producers that may outpace consumers |
Both implementations provide:
- event-driven workers with no polling sleeps;
- four task forms: procedures, methods, and indexed variants;
- timeout-aware
TryQueueandWaitForAlloverloads; - observable
Submit/TrySubmittask handles; - task batches and chunked
SubmitRangeprocessing; - race-safe cancellation of work that has not started;
- deterministic, draining
Shutdown; - captured worker exceptions through
LastError,Errors, andOnError; and - automatic worker-count selection with safety limits.
Important
🧵 Unix thread setup: On Linux and macOS, cthreads must be the first unit
in the program's uses clause. Without it, Free Pascal can compile
successfully but fail at runtime when the pool creates worker threads. Windows
does not need cthreads.
Use the managed global pool when you only need to submit work and wait for it:
program SimplePoolDemo;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
ThreadPool.Simple;
procedure ProcessItem(Index: Integer);
begin
WriteLn('Processing item ', Index);
end;
var
I: Integer;
begin
for I := 1 to 5 do
GlobalThreadPool.Queue(@ProcessItem, I);
GlobalThreadPool.WaitForAll;
end.GlobalThreadPool is created and destroyed by the unit. Do not free it
yourself.
Use a bounded pool when submission may need to wait for queue space:
program BoundedPoolDemo;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
ThreadPool.ProducerConsumer;
procedure DoWork;
begin
WriteLn('Working');
end;
var
Pool: TProducerConsumerThreadPool;
begin
Pool := TProducerConsumerThreadPool.Create(0, 1024);
try
if not Pool.TryQueue(@DoWork, 50) then
WriteLn('Queue remained full for 50 ms');
Pool.Shutdown; // close admission, drain accepted work, join workers
finally
Pool.Free;
end;
end.The first constructor argument is the worker count; 0 selects
TThread.ProcessorCount. The second is queue capacity.
Add ThreadPool.Tasks when work needs to be observed or coordinated:
uses
ThreadPool.Tasks, ThreadPool.Simple;
var
Task: IThreadPoolTask;
Batch: IThreadPoolTaskBatch;
begin
Task := GlobalThreadPool.Submit(@DoWork);
if Task.WaitFor(250) and (Task.State = ttsFailed) then
WriteLn(Task.ErrorMessage);
Batch := GlobalThreadPool.SubmitRange(@ProcessItem, 0, 999);
Batch.WaitFor;
end;Task.Cancel succeeds only while the task is pending. It never interrupts a
running callback. A range uses a small number of chunks by default instead of
creating one queue item per index. See the
task API for batch counts, timeouts, explicit
chunk sizes, and bounded-pool rules.
Both pools follow one monotonic lifecycle:
tpsAccepting -> tpsDraining -> tpsStopped
Shutdown stops new admission, waits for submissions already entering the
pool, drains every accepted task, wakes workers, and joins them. It is safe to
call more than once. Queueing after shutdown begins raises
EThreadPoolShutdown.
Pool.WaitForAll; // wait indefinitely
if not Pool.WaitForAll(250) then // milliseconds
WriteLn('Work remains');
Pool.Shutdown;Timeout values use these rules:
| Value | Meaning |
|---|---|
0 |
Immediate attempt or check |
| finite value | Maximum wait from call entry, in milliseconds |
THREADPOOL_INFINITE |
No deadline |
The Simple queue is unbounded, so its TryQueue timeout is present for API
symmetry and capacity cannot time out. For the bounded pool, TryQueue returns
False if no slot becomes available before the deadline. The legacy Queue
calls use a bounded compatibility wait and raise EQueueFullException when it
expires.
Warning
⏱️ Coordination matters: WaitForAll does not stop unrelated producer
threads from submitting more work. Coordinate producers first, or call
Shutdown to close admission before draining. Tasks and OnError callbacks
also have no automatic execution deadline; add cancellation or
application-level timeouts where needed.
Task handles and batches do not retain their pool. They may be kept after a pool is freed, because pool destruction drains accepted work first.
Task exceptions are caught so a worker failure does not terminate the pool.
var
MessageText: string;
begin
Pool.ClearErrors;
Pool.Queue(@RiskyWork);
Pool.WaitForAll;
for MessageText in Pool.Errors do
WriteLn(MessageText);
end;LastErroris the most recent message.Errorsis an oldest-first snapshot capped at 1000 entries.ErrorCountreports the stored count.ClearErrorsclears the collection andLastError.OnErrorfires synchronously on the worker that caught the exception. Keep callbacks short, bounded, and thread-safe.
| Form | Example |
|---|---|
| Procedure | Pool.Queue(@DoWork) |
| Object method | Pool.Queue(@Worker.DoWork) |
| Indexed procedure | Pool.Queue(@ProcessItem, I) |
| Indexed object method | Pool.Queue(@Worker.ProcessItem, I) |
Objects must remain alive until all queued methods that reference them have
finished. Call WaitForAll before freeing a callback target.
The library has no external dependencies.
- Add
srcto the project's unit search path, or installpackage/lazarus/threadpool_fp.lpkin Lazarus. - Add
ThreadPool.SimpleorThreadPool.ProducerConsumerto theusesclause. - On Unix-like systems, put
cthreadsfirst as shown in the quick start.
Requirements:
- Free Pascal 3.2.2 or later
- Lazarus 4.0 or later when using the package or project files
Build every example in Release mode from the repository root:
.\build-examples.ps1sh ./build-examples.shBoth scripts discover examples/*/*.lpi automatically and place the
executables in the ignored root-level example-bin/ directory. Pass Default
to build the default mode, or use -Rebuild in PowerShell / --rebuild in the
shell script to force a complete rebuild.
| Start with | Demonstrates |
|---|---|
Starter |
Smallest compilable program with explanatory comments |
SimpleDemo |
Procedures, methods, indexes, and the global pool |
ProdConSimpleDemo |
Basic bounded-pool ownership and queueing |
SimpleErrorHandlingBasic |
Reading captured errors after completion |
TaskCoordination |
Task handles, batches, ranges, and cancellation |
CoordinatedFileBackup |
Per-file progress, critical-failure policy, and pending cancellation |
ParallelLogAnalyzer |
Chunked analysis followed by a parallel reporting phase |
More focused samples cover:
- computation:
SimpleSquareNumbersandProdConSquareNumbers; - stateful processing:
SimpleThreadpoolDemo,SimpleWordCounter, andProdConMessageProcessor; - advanced callbacks:
SimpleErrorHandling; - real I/O:
ParallelFileHasher,ParallelUrlFetcher, andCoordinatedFileBackup; - coordinated data processing:
ParallelLogAnalyzer.
| Document | Purpose |
|---|---|
| Cheat sheet | Calls and safety rules at a glance |
| Simple API | Complete unbounded-pool reference |
| Producer-Consumer API | Complete bounded-pool reference |
| Tasks API | Submit, wait, batch, range, and cancellation contracts |
| Simple technical guide | Internal design and synchronization |
| Producer-Consumer technical guide | Queue and backpressure internals |
| v0.9.0 release notes | Current release scope and compatibility |
| Changelog | Full version history |
The banner's editable source is
docs/assets/threadpool-banner.svg; the
README displays its synchronized PNG render.
lazbuild package/lazarus/threadpool_fp.lpk
lazbuild tests/TestRunner.lpi
./tests/TestRunner -a -p --format=plainOn Windows, run tests/TestRunner.exe in the final command. CI builds the
package, tests, benchmark, and every example on Windows and Linux.
Bug reports, documentation improvements, examples, and code contributions are welcome. See CONTRIBUTING.md for the build, style, and pull request workflow.
ThreadPool for Free Pascal is available under the MIT License.
