ThreadPool.Simple — Technical Documentation
Architecture Overview#
graph TD
A1[Application] --> G[GlobalThreadPool]
A2[Application] --> S[TSimpleThreadPool]
subgraph "Interfaces"
ITP[IThreadPool]
IWT[IWorkerThread]
IWI[IWorkItem]
G & S -.->|implements| ITP
W1 & W2 & Wn -.->|implements| IWT
WI -.->|implements| IWI
end
subgraph "Thread Pool"
G & S --> |owns| TL[TThreadList]
G & S --> |owns| WQ[Dynamic circular<br>FIFO queue]
G & S --> |owns| CS[TCriticalSection<br>Work Item Count]
G & S --> |owns| EV[TEvent<br>Completion Signal]
G & S --> |owns| EL[TCriticalSection<br>Error Lock]
G & S --> |owns| EE[TEvent<br>Error Event]
TL --> |contains| W1[Worker Thread 1]
TL --> |contains| W2[Worker Thread 2]
TL --> |contains| Wn[Worker Thread n]
W1 & W2 & Wn -->|pop & execute| WQ
W1 & W2 & Wn -->|report errors| EL
end
subgraph "Work Items"
P1[TThreadProcedure] --> |wrapped as| WI[TWorkItem]
P2[TThreadMethod] --> |wrapped as| WI
P3[TThreadProcedureIndex] --> |wrapped as| WI
P4[TThreadMethodIndex] --> |wrapped as| WI
WI -->|queued in| WQ
end---
Core Components#
GlobalThreadPool#
A singleton TSimpleThreadPool declared in the unit's var section.
- Created in the unit
initializationblock — available from program startup - Freed in the unit
finalizationblock — never callGlobalThreadPool.Freemanually - Thread count defaults to
TThread.ProcessorCount(minimum 4)
TSimpleThreadPool#
The main pool class. Responsibilities:
- Owns and manages a list of
TSimpleWorkerThreadinstances (TThreadList) - Maintains a dynamically growing O(1) circular FIFO queue
- Tracks the number of pending work items with a
TCriticalSection+ counter - Signals
WaitForAllcallers via aTEventwhen the counter reaches zero - Captures worker exceptions and protects lifecycle transitions
TSimpleWorkerThread#
Each worker thread:
- Is created suspended and started explicitly by the pool constructor
- Blocks on
FWorkAvailableEventwhile the queue is empty - Drains available FIFO work after an enqueue wakes it
- Terminates cleanly when
Terminatedis set during pool destruction
Callback work items#
Both pools use the shared internal TThreadPoolCallbackWorkItem implementation. TSimpleWorkItem remains as a compatibility wrapper for code that named the old implementation type directly.
The shared work item wraps one of four task types:
| Type | Pascal type |
|---|---|
| Plain procedure | TThreadProcedure |
| Object method | TThreadMethod |
| Indexed procedure | TThreadProcedureIndex |
| Indexed method | TThreadMethodIndex |
On execution it calls the appropriate callback. The worker updates the pool's pending counter in a finally block, independently of callback failures.
---
Thread Count Rules#
| 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 — there is no dynamic scaling.
ProcessorCount limitations#
TThread.ProcessorCountis read once at program startup- It may count logical CPUs (hyper-threads), not physical cores
- It does not reflect runtime changes (e.g. CPU affinity, power states)
- Treat it as approximate guidance, not a precise thread budget
---
Usage Patterns#
1. Basic queuing#
GlobalThreadPool.Queue(@SimpleProcedure);
GlobalThreadPool.Queue(@MyObject.MyMethod);
GlobalThreadPool.Queue(@ProcessItem, 5);
GlobalThreadPool.WaitForAll;2. Error handling#
// GlobalThreadPool is managed by the unit — do NOT call Free on it.
GlobalThreadPool.Queue(@MyProcedure);
GlobalThreadPool.WaitForAll;
if GlobalThreadPool.LastError <> '' then
begin
WriteLn('Error: ', GlobalThreadPool.LastError);
GlobalThreadPool.ClearLastError; // reset before reuse
end;3. Custom pool#
var
Pool: TSimpleThreadPool;
begin
Pool := TSimpleThreadPool.Create(4);
try
Pool.Queue(@MyProcedure);
Pool.WaitForAll;
finally
Pool.Free;
end;
end;---
Thread Safety#
| Mechanism | Purpose |
|---|---|
TThreadList (threads) | Safe iteration and termination of worker threads |
TCriticalSection (FIFO queue) | Safe O(1) enqueue / dequeue across threads |
TCriticalSection (work item count) | Atomic increment/decrement of pending counter |
TEvent (completion) | Signals WaitForAll when counter hits zero |
TEvent (work available) | Wakes idle workers without polling |
| Lifecycle lock/events | Serialize admission, draining, and stopped state |
| Error lock | Protects LastError, Errors, and OnError |
---
Exception Handling#
How worker exceptions are captured#
Workers store the task error through TThreadPoolBase.SetLastError. Completion accounting runs in a separate finally block, and exceptions raised by an OnError handler are contained at the callback boundary.
Key behaviours#
LastErrorstores the raw exception message — no thread ID prefixLastErrorkeeps the most recent exception andErrorskeeps the bounded history- The pool keeps running after an exception — remaining tasks are processed
- Call
ClearLastErrorbefore reusing a pool to reset error state - Exceptions are not re-raised on the calling thread; check
LastErrorafterWaitForAll
Best practices#
- Always check
LastErrorafterWaitForAll - Call
ClearLastErrorbefore queuing a new batch if reusing the pool - Use
Errorswhen a batch can contain more than one failure
---
Performance Considerations#
- Idle workers block on an event and are woken immediately by submission
- Queue insertion/removal is O(1); growth only occurs when the ring is full
- For very large numbers of tiny tasks, consider batching them into fewer, larger work items to reduce queue overhead
- Thread count defaults to
ProcessorCount; raising it above that can hurt performance due to context-switching
---
Lifecycle#
The shared base state moves from tpsAccepting to tpsDraining to tpsStopped. Shutdown stops admission, waits for submissions already in progress, drains every accepted task, wakes and joins the workers, and publishes the stopped state. It is idempotent. Queueing after draining begins raises EThreadPoolShutdown, and a worker attempting to shut down its own pool is rejected before it can wait on itself.
---
Limitations#
- Exceptions are not propagated to the main thread — must poll
LastError - Thread count is fixed after construction — no dynamic scaling
- No task prioritisation or forced cancellation of running callbacks; v0.9 can cancel pending submitted work