A lightweight, easy-to-use thread pool implementation for Free Pascal. Simplify parallel processing for simple tasks! ⚡
Important
Parallel processing can improve performance for CPU-intensive tasks that can be executed independently. However, not all tasks benefit from parallelization. See Thread Management for important considerations.
Note
This library was originally written to explore the concept of thread pools in Free Pascal. It has since grown into a stable, tested implementation suitable for simple parallel processing tasks.
It is not designed for high-load or production-scale applications. For those use cases, see the alternatives below.
Tip
If you are looking for performant and battle-tested threading libraries, please check out these alternatives:
- Mormot2 Threading Library by @synopse
- ezthreads by @mr-highball
- OmniThreadLibrary by @gabr42 (Delphi-only)
- Or use threading in other languages via DLL/EXE calls;
- Go lang with Goroutines
- Python with concurrent.futures
- Rust with threadpool
- Any other language that supports modern threading
- 🚀 ThreadPool for Free Pascal
This library provides two thread pool implementations, each with its own strengths:
uses ThreadPool.Simple;- Global singleton instance for quick use
- Direct task execution
- Automatic thread count management
- Best for simple parallel tasks
- Lower memory overhead
uses ThreadPool.ProducerConsumer;A thread pool with fixed-size circular buffer (1024 items) and built-in backpressure handling:
-
Queue Management
- Fixed-size circular buffer for predictable memory usage
- Efficient space reuse without dynamic resizing
- Configurable capacity (default: 1024 items)
-
Backpressure Handling
- Load-based adaptive delays (10ms to 100ms)
- Automatic retry mechanism (up to 5 attempts)
- Throws EQueueFullException when retries exhausted
-
Monitoring & Debug
- Thread-safe error capture with thread IDs
- Detailed debug logging (can be disabled)
Warning
While the system includes automatic retry mechanisms, it's recommended that users implement their own error handling strategies for scenarios where the queue remains full after all retry attempts.
-
Thread Count Management
- Minimum 4 threads for optimal parallelism
- Maximum 2×
ProcessorCountto prevent overload - Fixed count after initialization
-
Task Types Support
- Simple procedures:
Pool.Queue(@MyProc) - Object methods:
Pool.Queue(@MyObject.MyMethod) - Indexed variants:
Pool.Queue(@MyProc, Index)
- Simple procedures:
-
Thread Safety
- Built-in synchronization
- Safe resource sharing
- Protected error handling
-
Error Management
- Worker exceptions are caught automatically; the pool keeps running
LastErrorfor the most recent failureErrorscollection captures all failed-task messages (capped, oldest dropped) — v0.7.0- Optional
OnErrorcallback fired per failed task — v0.7.0
Note
Thread count is determined by TThread.ProcessorCount at startup and remains fixed. See Thread Management for details.
Important
On Linux/macOS, your program must use the cthreads unit — and it must be the first unit in your program's uses clause.
Free Pascal does not install a threading manager by default on Unix-like systems. Without cthreads, creating the thread pool fails at runtime with an access violation (exit code 217). Windows does not need it.
program MyApp;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads, // MUST be first on Linux/macOS
{$ENDIF}
ThreadPool.Simple; // or ThreadPool.ProducerConsumerThe examples in this repository already include this guard — see examples/Starter/Starter.lpr.
From the official FPC documentation: "The cthreads unit simply needs to be included in the uses clause of the program, preferably the very first unit, and the initialization section of the unit will do all the work."
uses
{$IFDEF UNIX}cthreads,{$ENDIF} // see the note above — required on Linux/macOS
ThreadPool.Simple;
// Simple parallel processing
procedure ProcessItem(index: Integer);
begin
WriteLn('Processing item: ', index);
end;
begin
// Queue multiple items
for i := 1 to 5 do
GlobalThreadPool.Queue(@ProcessItem, i);
GlobalThreadPool.WaitForAll;
end;uses
{$IFDEF UNIX}cthreads,{$ENDIF} // see the note above — required on Linux/macOS
ThreadPool.ProducerConsumer;
procedure DoWork;
begin
WriteLn('Working in thread: ', GetCurrentThreadId);
end;
var
Pool: TProducerConsumerThreadPool;
begin
Pool := TProducerConsumerThreadPool.Create;
try
Pool.Queue(@DoWork);
Pool.WaitForAll;
finally
Pool.Free;
end;
end;program ErrorHandling;
{$mode objfpc}{$H+}{$J-}
uses
Classes, SysUtils, ThreadPool.Simple;
procedure RiskyProcedure;
begin
raise Exception.Create('Something went wrong!');
end;
var
Pool: TSimpleThreadPool;
begin
Pool := TSimpleThreadPool.Create(4); // Create with 4 threads
try
Pool.Queue(@RiskyProcedure);
Pool.WaitForAll;
// Check for errors after completion
if Pool.LastError <> '' then
begin
WriteLn('An error occurred: ', Pool.LastError);
Pool.ClearLastError; // Clear for reuse if needed
end;
finally
Pool.Free;
end;
end.program ErrorHandling;
{$mode objfpc}{$H+}{$J-}
uses
Classes, SysUtils, ThreadPool.ProducerConsumer;
procedure RiskyProcedure;
begin
raise Exception.Create('Something went wrong!');
end;
var
Pool: TProducerConsumerThreadPool;
begin
Pool := TProducerConsumerThreadPool.Create;
try
try
Pool.Queue(@RiskyProcedure);
except
on E: EQueueFullException do
WriteLn('Queue is full after retries: ', E.Message);
end;
Pool.WaitForAll;
// Check for errors after completion
if Pool.LastError <> '' then
begin
WriteLn('An error occurred: ', Pool.LastError);
Pool.ClearLastError; // Clear for reuse if needed
end;
finally
Pool.Free;
end;
end.LastError only holds the most recent failure. To inspect every failed
task, use the Errors collection (oldest first, capped at MAX_STORED_ERRORS = 1000).
This works the same on both pools:
var
Msg: string;
begin
Pool.ClearErrors;
for i := 0 to 9 do
Pool.Queue(@RiskyProc, i);
Pool.WaitForAll;
WriteLn(Pool.ErrorCount, ' task(s) failed:');
for Msg in Pool.Errors do
WriteLn(' - ', Msg);
Pool.ClearErrors; // resets the collection and LastError
end;Prefer to react the moment a task fails (instead of polling after WaitForAll)?
Assign an OnError callback:
// IMPORTANT: OnError is called from a worker thread. Keep the handler short and
// thread-safe; synchronize if it touches the UI or shared state.
Pool.OnError := @MyHandler.OnTaskError;Note
Error Handling
- 🛡️ Exceptions are caught and stored with thread IDs
- ⚡ Pool continues operating after exceptions
- 🔄 Use ClearLastError to reset error state
Debugging
- 🔍 Error messages contain thread identification
- 📝 Debug logging enabled by default (configurable)
- 📊 Queue capacity monitoring available
Need a thread pool?
├─ Tasks are fire-and-forget, count is predictable, low overhead wanted?
│ └─ → Use ThreadPool.Simple (or the GlobalThreadPool singleton)
└─ Producer can outpace consumers, or you need queue overflow control?
└─ → Use ThreadPool.ProducerConsumer
Use Simple Thread Pool when:
- Direct task execution without queuing needed
- Task count is predictable and moderate
- Low memory overhead is important
- Global instance (GlobalThreadPool) convenience desired
- Simple error handling is sufficient
Use Producer-Consumer Pool when:
- High volume of tasks with rate control needed
- Backpressure handling required
- Queue overflow protection important
- Need detailed execution monitoring
- Want configurable retry mechanisms
All four Queue overloads share the same pattern — pick the one that fits your task:
| Overload | Signature | Use when | Example |
|---|---|---|---|
| Plain procedure | Queue(@MyProc) |
Standalone procedure, no shared state needed | File I/O, independent calculations |
| Object method | Queue(@MyObj.MyMethod) |
Task needs access to object fields/state | Counter objects, result accumulators |
| Indexed procedure | Queue(@MyProc, i) |
Loop parallelism over an array/range | for i := 0 to N-1 do Queue(@Proc, i) |
| Indexed method | Queue(@MyObj.MyMethod, i) |
Loop parallelism + object state | Parallel array transform on an object |
Note
LastError holds only the most recent exception. To see every failed task, use the Errors collection (added in v0.7.0) — see Capturing all task errors. Call ClearErrors before reusing the pool.
- 👋 Starter (
examples/Starter/Starter.lpr)- The absolute minimum to compile and run
- Heavily commented — every line explained
- Best first file to read before the other examples
-
🎓 Simple Demo (
examples/SimpleDemo/SimpleDemo.lpr)- Basic usage with GlobalThreadPool
- Demonstrates procedures and methods
- Shows proper object lifetime
-
🔢 Thread Pool Demo (
examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr)- Custom thread pool management
- Thread-safe operations
- Error handling patterns
-
📝 Word Counter (
examples/SimpleWordCounter/SimpleWordCounter.lpr)- Queue-based task processing
- Thread-safe counters
- File I/O with queue management
-
🔢 Square Numbers (
examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr)- High volume task processing
- Queue full handling
- Performance comparison
-
🛡️ Error Handling — Basic (
examples/SimpleErrorHandlingBasic/SimpleErrorHandlingBasic.lpr)- Start here for error handling — the easy way
- Just queue,
WaitForAll, then readErrors/ErrorCount/LastError - No callback, no custom class, no locking required
-
🛡️ Error Handling — Advanced (
examples/SimpleErrorHandling/SimpleErrorHandling.lpr)- Adds the
OnErrorcallback to react while tasks run - Shows the thread-safe handler pattern (needed only because the handler keeps shared state)
- Adds the
-
📁 Parallel File Hasher (
examples/ParallelFileHasher/ParallelFileHasher.lpr)- Hashes multiple files concurrently with
ThreadPool.Simple - Demonstrates real file I/O failures captured through
Errors/OnError - Creates and cleans up its own sample files
- Hashes multiple files concurrently with
-
🎓 Simple Demo (
examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr)- Basic usage with ProducerConsumerThreadPool
- Demonstrates procedures and methods
- Shows proper object lifetime
-
🔢 Square Numbers (
examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr)- High volume task processing
- Queue full handling
- Backpressure demonstration
- Performance monitoring
-
📝 Message Processor (
examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr)- Queue-based task processing
- Thread-safe message handling
- Graceful shutdown
- Error handling patterns
-
🌐 Parallel URL Fetcher (
examples/ParallelUrlFetcher/ParallelUrlFetcher.lpr)- Fetches multiple URLs concurrently with
ThreadPool.ProducerConsumer - Demonstrates real HTTP/DNS failures captured through
Errors/OnError - Uses one HTTP client per task so clients are not shared across threads
- Fetches multiple URLs concurrently with
-
Add the
srcdirectory to your project's search path -
Choose your implementation:
For Simple Thread Pool:
uses {$IFDEF UNIX}cthreads,{$ENDIF} // required on Linux/macOS (must be first) ThreadPool.Simple;
For Producer-Consumer Thread Pool:
uses {$IFDEF UNIX}cthreads,{$ENDIF} // required on Linux/macOS (must be first) ThreadPool.ProducerConsumer;
-
Start using:
- Simple: Use
GlobalThreadPoolor createTSimpleThreadPool - Producer-Consumer: Create
TProducerConsumerThreadPool
- Simple: Use
Compile and run the simplest demo from the command line to confirm everything is wired up correctly:
# Using the Free Pascal compiler directly
fpc -Fu./src examples/SimpleDemo/SimpleDemo.lpr && ./SimpleDemo
# Or build with Lazarus from the command line
lazbuild examples/SimpleDemo/SimpleDemo.lpi && ./SimpleDemoExpected output (order may vary — tasks run in parallel):
Demo of ThreadPool functionality:
--------------------------------
1. Queueing simple procedure
2. Queueing method of a class
3. Queueing indexed procedure
4. Queueing method with index of a class
--------------------------------
Waiting for all tasks to complete...
Simple procedure executed
Method executed
Indexed procedure executed with index: 1
Method with index executed: 2
--------------------------------
All tasks completed successfully!
Tip
Make sure your source file starts with {$mode objfpc}{$H+}. Without this, Free Pascal defaults to TP/Delphi-7 mode and some syntax will not compile.
On Linux/macOS, also ensure {$IFDEF UNIX}cthreads{$ENDIF} is the first unit in your program's uses clause (see the Quick Start note). Forgetting it causes a runtime access violation, not a compile error — so the build succeeds but the program crashes when it creates the pool.
- 💻 Free Pascal 3.2.2 or later
- 📦 Lazarus 3.6.0 or later
- 🆓 No external dependencies
- ThreadPool.Simple API Documentation
- ThreadPool.Simple Technical Details
- ThreadPool.ProducerConsumer API Documentation
- ThreadPool.ProducerConsumer Technical Details
- Go to the
tests/directory - Open
TestRunner.lpiin Lazarus IDE and compile - Run
./TestRunner.exe -a -p --format=plainto see the test results. - Ensure all tests pass to verify the library's functionality
May take up to 5 mins to run all tests.
- Default: Uses ProcessorCount when thread count ≤ 0
- Minimum: 4 threads enforced
- Maximum: 2× ProcessorCount
- Fixed after creation (no dynamic scaling)
Simple Thread Pool
- Direct task execution without queuing
- Continuous task processing
- Clean shutdown handling
Producer-Consumer Thread Pool
- Fixed-size circular queue (1024 items by default, configurable)
- Backpressure handling with adaptive delays
- Graceful overflow management
// WRONG — MyObject may be freed while worker threads are still calling its methods
MyObject := TMyClass.Create;
GlobalThreadPool.Queue(@MyObject.DoWork);
MyObject.Free; // freed too early!
GlobalThreadPool.WaitForAll;
// CORRECT — always wait before freeing
MyObject := TMyClass.Create;
try
GlobalThreadPool.Queue(@MyObject.DoWork);
GlobalThreadPool.WaitForAll; // wait first
finally
MyObject.Free; // safe to free now
end;Without WaitForAll, your program may exit (and destroy the pool) while tasks are still running, causing access violations or silent data loss.
// WRONG
for i := 0 to 99 do
GlobalThreadPool.Queue(@ProcessItem, i);
// program exits here, tasks may never finish
// CORRECT
for i := 0 to 99 do
GlobalThreadPool.Queue(@ProcessItem, i);
GlobalThreadPool.WaitForAll;LastError is overwritten on every exception. If multiple tasks fail and you
only check LastError, you see just the last one. Use the Errors collection
to capture all of them — see Capturing all task errors below.
// Queue several tasks that might fail
for i := 0 to 9 do
Pool.Queue(@RiskyProc, i);
Pool.WaitForAll;
// Only the LAST exception is in LastError...
if Pool.LastError <> '' then
WriteLn('Most recent failure: ', Pool.LastError);
// ...but Errors has them all:
WriteLn(Pool.ErrorCount, ' task(s) failed in total');
Pool.ClearErrors;GlobalThreadPool is managed by the unit's initialization/finalization blocks. Do not call GlobalThreadPool.Free — let the runtime clean it up.
// WRONG
GlobalThreadPool.Free; // double-free at program exit!
// CORRECT — just use it; finalization handles cleanup
GlobalThreadPool.Queue(@MyProc);
GlobalThreadPool.WaitForAll;- Performance & robustness pass — event-driven idle workers (remove the Simple pool's poll loop), shutdown review, stress/soak tests (planned for 0.8.0)
- Support for
procedure Queue(AMethod: TProc; AArgs: array of Const); - More comprehensive tests
- More examples
Contributions are welcome — bug reports, docs, examples, and code. See CONTRIBUTING.md for how to build, test, and submit changes.
Special thanks to the Free Pascal and Lazarus communities and the creators of the threading libraries mentioned above for inspiration!
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for the full version history.
💡 More Tip: Check out the examples directory for more usage patterns!