ThreadPool for Free Pascal — cheat sheet

Quick reference for v0.9.1. For full contracts, use the Simple API or Producer-Consumer API. Task handles, batches, ranges, and cancellation are covered by the Tasks API.

Choose a pool#

NeedUse
Straightforward fire-and-forget work and an unbounded FIFOThreadPool.Simple
A ready-to-use process-wide instanceGlobalThreadPool from ThreadPool.Simple
Bounded memory and explicit queue saturation handlingThreadPool.ProducerConsumer
Producer backpressure with a submission deadlineTProducerConsumerThreadPool.TryQueue
Inspect bounded queue usageQueueCount, QueueCapacity, and QueueLoadFactor
Observe or cancel one pending taskSubmit and IThreadPoolTask
Coordinate related task handlesIThreadPoolTaskBatch
Process an integer range efficientlySubmitRange

Import correctly#

On Linux and macOS, cthreads must be the first unit in the program's uses clause. Windows does not need it.

Pascal
uses
  {$IFDEF UNIX}
  cthreads,
  {$ENDIF}
  ThreadPool.Tasks,
  ThreadPool.Simple;  // or ThreadPool.ProducerConsumer

Create a pool#

Pascal
// Managed singleton; do not free it yourself.
GlobalThreadPool.Queue(@DoWork);

// Private unbounded pool.
SimplePool := TSimpleThreadPool.Create(4);

// Private bounded pool: worker count, queue capacity.
BoundedPool := TProducerConsumerThreadPool.Create(4, 1024);

Pass 0 as the worker count to use TThread.ProcessorCount. The library always creates at least four workers; positive requests are capped at 2 * TThread.ProcessorCount before that minimum is applied.

Queue work#

Task formCall
ProcedurePool.Queue(@DoWork)
Object methodPool.Queue(@Worker.DoWork)
Indexed procedurePool.Queue(@ProcessItem, Index)
Indexed object methodPool.Queue(@Worker.ProcessItem, Index)

The indexed callback signatures are:

Pascal
procedure ProcessItem(Index: Integer);
procedure TWorker.ProcessItem(Index: Integer);

Submit observable work#

Pascal
Task := Pool.Submit(@DoWork);

if not Task.WaitFor(250) then
  WriteLn('Still pending or running')
else if Task.State = ttsFailed then
  WriteLn(Task.ErrorMessage);

States are ttsPending, ttsRunning, ttsCompleted, ttsFailed, and ttsCancelled. WaitFor returns True for any terminal state.

Pascal
if Task.Cancel then
  WriteLn('Cancelled before worker start');

Cancellation never interrupts running code. A cancelled entry may remain in the queue as a skipped tombstone until a worker reaches it.

Coordinate a batch#

Pascal
Batch := NewThreadPoolTaskBatch;
for I := 0 to High(Items) do
  Batch.Add(Pool.Submit(@ProcessItem, I));

Batch.WaitFor;
WriteLn(Batch.FinishedCount, '/', Batch.Count);

Batch.CancelPending returns the number of pending tasks it cancelled. A wait uses the batch entries present when the call starts and one overall timeout.

Submit a chunked range#

Pascal
Batch := Pool.SubmitRange(@ProcessItem, 0, High(Items));
Batch.WaitFor;

Bounds are inclusive. The default chunk size 0 creates at most four chunks per worker. Pass a positive size for explicit chunks; smaller chunks improve load balance and cancellation granularity but add queue overhead.

Handle bounded-queue saturation#

Prefer TryQueue when using the Producer-Consumer pool:

Pascal
if not Pool.TryQueue(@DoWork, 50) then
  WriteLn('Queue remained full for 50 ms');

Queue(...) remains available, but it uses a bounded compatibility wait and raises EQueueFullException if that wait expires. The Simple pool is unbounded, so its TryQueue timeout exists for API symmetry and capacity does not time out.

Wait and shut down#

Pascal
Pool.WaitForAll;                 // wait indefinitely

if not Pool.WaitForAll(250) then
  WriteLn('Work remains after 250 ms');

Pool.Shutdown;                   // stop admission, drain, join workers

Timeouts are milliseconds:

ValueMeaning
0Immediate attempt or state check
finite valueMaximum wait from call entry
THREADPOOL_INFINITENo deadline

After shutdown begins, Queue and TryQueue raise EThreadPoolShutdown. Repeated Shutdown calls are safe. Do not call Shutdown from one of the pool's own worker tasks.

Read task errors#

Worker exceptions are captured; they do not terminate the pool.

Pascal
var
  MessageText: string;
begin
  Pool.ClearErrors;
  Pool.Queue(@RiskyWork);
  Pool.WaitForAll;

  for MessageText in Pool.Errors do
    WriteLn(MessageText);
end;
  • LastError is only the most recent message.
  • Errors is an oldest-first snapshot, capped at MAX_STORED_ERRORS = 1000.
  • ErrorCount reports the stored count.
  • ClearErrors clears both the collection and LastError.
  • OnError runs synchronously on the worker that caught the exception; keep the handler short, bounded, and thread-safe.

Keep objects alive#

Pascal
Worker := TWorker.Create;
try
  Pool.Queue(@Worker.DoWork);
  Pool.WaitForAll;  // wait before freeing the callback target
finally
  Worker.Free;
end;

Also remember:

  • Do not free GlobalThreadPool; its unit owns it.
  • WaitForAll is not an admission barrier for unrelated producers. Coordinate producers first, or use Shutdown to close admission before draining.
  • Tasks and OnError callbacks have no automatic execution deadline. Add cancellation or application-level timeouts to operations that may block.

Build and test#

BASH
lazbuild package/lazarus/threadpool_fp.lpk
lazbuild tests/TestRunner.lpi
./tests/TestRunner -a -p --format=plain

On Windows, run tests/TestRunner.exe in the final command.

More detail#