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
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)
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
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
A lightweight wrapper around one of the four task types:
| Type | Pascal type |
|---|---|
| Plain procedure | TThreadProcedure |
| Object method | TThreadMethod |
| Indexed procedure | TThreadProcedureIndex |
| Indexed method | TThreadMethodIndex |
On execution it calls the appropriate callable, then decrements the pool's pending counter (and signals completion if it reaches zero).
| 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.
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
GlobalThreadPool.Queue(@SimpleProcedure);
GlobalThreadPool.Queue(@MyObject.MyMethod);
GlobalThreadPool.Queue(@ProcessItem, 5);
GlobalThreadPool.WaitForAll;// 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;var
Pool: TSimpleThreadPool;
begin
Pool := TSimpleThreadPool.Create(4);
try
Pool.Queue(@MyProcedure);
Pool.WaitForAll;
finally
Pool.Free;
end;
end;| 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 |
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.
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
- Always check
LastErrorafterWaitForAll - Call
ClearLastErrorbefore queuing a new batch if reusing the pool - Use
Errorswhen a batch can contain more than one failure
- 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
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.
- Exceptions are not propagated to the main thread — must poll
LastError - Thread count is fixed after construction — no dynamic scaling
- No task prioritisation or cancellation