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
- Thread-specific error capture
- Error messages with thread IDs
- Continuous operation after exceptions
Note
Thread count is determined by TThread.ProcessorCount at startup and remains fixed. See Thread Management for details.
uses 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 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.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 is overwritten (not appended) each time a task raises an exception. If you queue multiple tasks, only the last error is stored. Check LastError immediately after WaitForAll and call ClearLastError 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
-
🎓 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
-
Add the
srcdirectory to your project's search path -
Choose your implementation:
For Simple Thread Pool:
uses ThreadPool.Simple;For Producer-Consumer Thread Pool:
uses 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.
- 💻 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 — not appended. If multiple tasks fail, you only see the last one.
// 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('At least one task failed: ', Pool.LastError);
Pool.ClearLastError;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;- Adaptive thread adjustment based on a load factor
- Support for
procedure Queue(AMethod: TProc; AArgs: array of Const); - More comprehensive tests
- More examples
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!