ThreadPool.ProducerConsumer implements a thread pool backed by a fixed-size
circular queue with built-in backpressure. Use it when task production can
outpace consumption and you need predictable memory usage and overflow control.
For simpler use cases see ThreadPool.Simple.
Linux/macOS: your program must list
cthreadsas the first unit in itsusesclause, or creating the pool will raise a runtime access violation (exit code 217). Windows does not need it.uses {$IFDEF UNIX}cthreads,{$ENDIF} // must be first on Unix-like systems ThreadPool.ProducerConsumer;
constructor Create(AThreadCount: Integer = 0; AQueueSize: Integer = 1024);| Parameter | Default | Description |
|---|---|---|
AThreadCount |
0 (uses CPU count) |
Worker threads. Clamped: min 4, max 2× ProcessorCount |
AQueueSize |
1024 |
Circular queue capacity in work items |
// Default: CPU-count threads, 1024-item queue
Pool := TProducerConsumerThreadPool.Create;
// Custom: 4 threads, 512-item queue
Pool := TProducerConsumerThreadPool.Create(4, 512);All four overloads are thread-safe. A call waits only while the bounded queue is
actually full. If its compatibility timeout expires, EQueueFullException is
raised.
procedure Queue(AProcedure: TThreadProcedure);
procedure Queue(AMethod: TThreadMethod);
procedure Queue(AProcedure: TThreadProcedureIndex; AIndex: Integer);
procedure Queue(AMethod: TThreadMethodIndex; AIndex: Integer);Pool.Queue(@MyProcedure); // plain procedure
Pool.Queue(@MyObject.MyMethod); // object method
Pool.Queue(@MyIndexedProc, 42); // indexed procedure
Pool.Queue(@MyObject.MyMethod, 42); // indexed methodUse the matching TryQueue overloads when queue saturation is expected. They
return False instead of raising:
if not Pool.TryQueue(@MyProcedure, 50) then
WriteLn('No queue space became available within 50 ms');Timeouts are milliseconds. 0 does not wait and THREADPOOL_INFINITE waits
without a deadline.
TThreadProcedure = procedure;
TThreadMethod = procedure of object;
TThreadProcedureIndex = procedure(index: Integer);
TThreadMethodIndex = procedure(index: Integer) of object;Blocks until every queued task has finished executing.
Pool.WaitForAll;if not Pool.WaitForAll(250) then
WriteLn('Tasks are still running');WaitForAll does not close admission. Coordinate concurrent producers first,
or use Shutdown to atomically stop admission before draining.
Always call before freeing the pool or any objects whose methods were queued.
Two distinct error paths exist — handle both:
EQueueFullException is raised synchronously on the calling thread when the
queue stays full until the compatibility timeout expires. Catch it around each Queue call,
not around WaitForAll.
try
Pool.Queue(@MyProcedure);
except
on E: EQueueFullException do
begin
// Queue is saturated. Options:
// - wait and retry: Pool.WaitForAll; Pool.Queue(@MyProcedure);
// - log and skip
WriteLn('Queue full: ', E.Message);
end;
end;Always catch by type (EQueueFullException), not by message string.
Exceptions that occur inside a task are caught by the worker thread and stored
in LastError. Check after WaitForAll.
Pool.WaitForAll;
if Pool.LastError <> '' then
begin
// LastError holds the raw message of the most recent worker exception.
WriteLn('Task error: ', Pool.LastError);
Pool.ClearLastError; // reset before reuse
end;To inspect every task failure (not just the last), use the Errors
collection added in v0.7.0 — oldest first, capped at MAX_STORED_ERRORS = 1000:
var
Msg: string;
begin
Pool.ClearErrors;
// ... queue tasks ...
Pool.WaitForAll;
WriteLn(Pool.ErrorCount, ' task(s) failed:');
for Msg in Pool.Errors do
WriteLn(' - ', Msg);
Pool.ClearErrors; // resets the collection and LastError
end;Or assign OnError to react the moment a task fails, instead of polling:
// Called synchronously from a worker thread — keep it short, bounded, and
// thread-safe.
Pool.OnError := @Handler.OnTaskError;Exceptions raised by the handler are contained by the pool. They cannot terminate a worker or prevent task completion accounting.
Containment does not limit callback execution time. A task or OnError handler
that blocks continues to occupy its worker, and can delay WaitForAll and
Shutdown. Apply an application-level timeout or cancellation mechanism to
operations that may block.
var
Pool: TProducerConsumerThreadPool;
begin
Pool := TProducerConsumerThreadPool.Create;
try
try
Pool.Queue(@RiskyOperation);
except
on E: EQueueFullException do
WriteLn('Queue full: ', E.Message);
end;
Pool.WaitForAll;
if Pool.LastError <> '' then
WriteLn('Task failed: ', Pool.LastError);
finally
Pool.Free;
end;
end;property ThreadCount: Integer; // read-only; number of worker threads
property LastError: string; // read-only; last worker exception message
property Errors: TStringArray; // read-only; all captured messages (capped)
property ErrorCount: Integer; // read-only; count of messages in Errors
property OnError: TThreadPoolErrorEvent; // fired (on a worker thread) per failed task
property WorkQueue: TThreadSafeQueue; // access to queue for monitoring/config
property State: TThreadPoolState; // accepting, draining, or stopped
function TryQueue(...; ATimeoutMS: Cardinal): Boolean; // four matching overloads
procedure WaitForAll; overload;
function WaitForAll(ATimeoutMS: Cardinal): Boolean; overload;
procedure Shutdown;
procedure ClearLastError;
procedure ClearErrors; // clears both Errors and LastErrorv0.8.0 replaces load-threshold sleeps with event-driven not-full signalling.
TBackpressureConfig remains available so existing source continues to compile.
MaxAttempts and HighLoadDelay determine the compatibility wait used by the
legacy Queue overloads; new code should express its deadline directly with
TryQueue(..., TimeoutMS). Threshold and low/medium-delay fields are retained
for source compatibility and no longer introduce sleeps.
type
TBackpressureConfig = record
LowLoadThreshold: Double; // Default: 0.5 — delay starts at 50% capacity
MediumLoadThreshold: Double; // Default: 0.7 — medium delay at 70%
HighLoadThreshold: Double; // Default: 0.9 — max delay at 90%
LowLoadDelay: Integer; // Default: 10 ms
MediumLoadDelay: Integer; // Default: 50 ms
HighLoadDelay: Integer; // Default: 100 ms
MaxAttempts: Integer; // Default: 5 — raises EQueueFullException after this
end;Read and write the config through WorkQueue.BackpressureConfig:
var
Config: TBackpressureConfig;
begin
Config := Pool.WorkQueue.BackpressureConfig;
Config.MaxAttempts := 3;
Config.HighLoadDelay := 200;
Pool.WorkQueue.BackpressureConfig := Config;
end;The DEBUG_LOG variable controls verbose output:
DEBUG_LOG := True; // opt in while diagnosing scheduler behaviourDebug logging is disabled by default so normal workloads and benchmarks do not pay for synchronized console output.
Shutdown atomically stops admission, lets submissions already in progress
finish enqueueing, drains all accepted tasks, wakes and joins every worker, and
sets State to tpsStopped. It is idempotent and is called automatically by
the destructor. Queueing after shutdown raises EThreadPoolShutdown.
| Condition | Result |
|---|---|
AThreadCount <= 0 |
Uses TThread.ProcessorCount |
AThreadCount < 4 |
Raised to 4 (minimum enforced) |
AThreadCount > 2 × ProcessorCount |
Clamped to 2 × ProcessorCount |
Thread count is fixed after construction.
// WRONG — the message includes dynamic content and will never match literally
on E: Exception do
if E.Message = 'Queue is full' then ...
// CORRECT — catch by exception type
on E: EQueueFullException do ...// WRONG — worker thread may still be calling MyObject.MyMethod
Pool.Queue(@MyObject.MyMethod);
MyObject.Free;
// CORRECT
try
Pool.Queue(@MyObject.MyMethod);
Pool.WaitForAll;
finally
MyObject.Free;
end;// WRONG — if Queue raises, WaitForAll is never reached, pool is freed while busy
try
Pool.Queue(@MyProc);
Pool.WaitForAll; // skipped on exception
except
on E: EQueueFullException do ...
end;
Pool.Free;
// CORRECT — separate concerns
try
Pool.Queue(@MyProc);
except
on E: EQueueFullException do
WriteLn('Queue full: ', E.Message);
end;
Pool.WaitForAll; // always reached
Pool.Free;Queue multiple items and handle overflow per-item:
var
Pool: TProducerConsumerThreadPool;
i: Integer;
begin
Pool := TProducerConsumerThreadPool.Create(4, 512);
try
for i := 1 to 2000 do
begin
try
Pool.Queue(@MyProc);
except
on E: EQueueFullException do
begin
Pool.WaitForAll; // let the queue drain
Pool.Queue(@MyProc); // retry
end;
end;
end;
Pool.WaitForAll;
finally
Pool.Free;
end;
end;- Fixed queue capacity — no dynamic resizing
LastErrorholds only the most recent worker exception (useErrorsto collect all; capped atMAX_STORED_ERRORS)- No task priority or cancellation support
- No dynamic thread scaling after construction
- Not suitable for real-time or UI-thread work