Skip to content

Latest commit

 

History

History
365 lines (281 loc) · 8.79 KB

File metadata and controls

365 lines (281 loc) · 8.79 KB

ThreadPool.Simple API Documentation

Linux/macOS: your program must list cthreads as the first unit in its uses clause, 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.Simple;

See the official FPC documentation on cthreads.

Thread Pool Types

GlobalThreadPool

A ready-to-use singleton instance declared in the ThreadPool.Simple unit.

  • Created automatically at program startup — do not call GlobalThreadPool.Free
  • Uses the default thread count (ProcessorCount, minimum 4)
  • Suitable for most applications
uses ThreadPool.Simple;

GlobalThreadPool.Queue(@MyProcedure);
GlobalThreadPool.WaitForAll;

The timeout overload returns False if work remains after the requested number of milliseconds:

if not GlobalThreadPool.WaitForAll(250) then
  WriteLn('Still running');

0 is an immediate check. THREADPOOL_INFINITE waits indefinitely.

WaitForAll does not close admission. Coordinate concurrent producers first, or use Shutdown to atomically stop admission before draining.

TSimpleThreadPool

A manually managed pool for when you need explicit control over thread count or lifetime.

  • Create with TSimpleThreadPool.Create(AThreadCount)
  • Thread count is clamped: minimum 4, maximum 2× ProcessorCount
  • Multiple independent pools can coexist
  • Must be freed by the caller
uses ThreadPool.Simple;

var
  Pool: TSimpleThreadPool;
begin
  Pool := TSimpleThreadPool.Create(4);
  try
    Pool.Queue(@MyProcedure);
    Pool.WaitForAll;
  finally
    Pool.Free;
  end;
end;

Queue Overloads

All four Queue overloads are thread-safe and can be called from any thread.

// 1. Plain procedure — no shared state needed
GlobalThreadPool.Queue(@MyProcedure);

// 2. Object method — task needs access to object fields
GlobalThreadPool.Queue(@MyObject.MyMethod);

// 3. Indexed procedure — loop parallelism
GlobalThreadPool.Queue(@MyIndexedProcedure, 42);

// 4. Indexed method — loop parallelism + object state
GlobalThreadPool.Queue(@MyObject.MyIndexedMethod, 42);

Type signatures (from ThreadPool.Types)

TThreadProcedure      = procedure;
TThreadMethod         = procedure of object;
TThreadProcedureIndex = procedure(index: Integer);
TThreadMethodIndex    = procedure(index: Integer) of object;

WaitForAll

Blocks the calling thread until every queued task has finished executing.

GlobalThreadPool.WaitForAll;

Always call WaitForAll before:

  • Reading results written by worker tasks
  • Freeing objects whose methods were queued
  • Calling LastError

Error Handling

Worker thread exceptions are caught automatically and stored in LastError. The pool continues processing remaining tasks after an exception.

var
  Pool: TSimpleThreadPool;
begin
  Pool := TSimpleThreadPool.Create(4);
  try
    Pool.Queue(@RiskyProcedure);
    Pool.WaitForAll;

    if Pool.LastError <> '' then
    begin
      // LastError holds the raw exception message of the most recent failure.
      WriteLn('Error: ', Pool.LastError);
      Pool.ClearLastError;  // Reset before reusing the pool
    end;
  finally
    Pool.Free;
  end;
end;

Collecting all errors (v0.7.0)

LastError only holds the most recent failure. To inspect every task error, use the Errors collection (oldest first, capped at MAX_STORED_ERRORS = 1000):

var
  Msg: string;
begin
  Pool.ClearErrors;
  for i := 0 to N - 1 do
    Pool.Queue(@RiskyProcedure);
  Pool.WaitForAll;

  WriteLn(Pool.ErrorCount, ' task(s) failed:');
  for Msg in Pool.Errors do
    WriteLn('  - ', Msg);

  Pool.ClearErrors;  // resets the collection and LastError
end;

Reacting to errors as they happen (v0.7.0)

Assign OnError to be notified the moment a task fails, instead of polling after WaitForAll:

type
  TMyHandler = class
    procedure OnTaskError(const AMessage: string);
  end;

procedure TMyHandler.OnTaskError(const AMessage: string);
begin
  // NOTE: called synchronously from a worker thread. Keep it short, bounded,
  // and thread-safe; synchronize if you touch the UI or shared state.
  Log('task failed: ' + AMessage);
end;

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.

Lifecycle and submission timeouts (v0.8.0)

TSimpleThreadPool is unbounded, so TryQueue normally succeeds immediately; the timeout argument exists so code can use either pool through IThreadPool. After shutdown begins, Queue and TryQueue raise EThreadPoolShutdown.

Pool.TryQueue(@MyProcedure, 0);
Pool.Shutdown; // stop admission, drain accepted work, join workers

State progresses once from tpsAccepting to tpsDraining to tpsStopped. Shutdown is idempotent and destruction calls it automatically.

Properties

property LastError: string;            // Raw message of the most recent worker exception
property Errors: TStringArray;         // All captured messages (oldest first, capped)
property ErrorCount: Integer;          // Number of messages currently in Errors
property OnError: TThreadPoolErrorEvent; // Fired (on a worker thread) per failed task
property ThreadCount: Integer;         // Number of worker threads (read-only)
property State: TThreadPoolState;       // Accepting, draining, or stopped

Methods

procedure Queue(AProcedure: TThreadProcedure);
procedure Queue(AMethod: TThreadMethod);
procedure Queue(AProcedure: TThreadProcedureIndex; AIndex: Integer);
procedure Queue(AMethod: TThreadMethodIndex; AIndex: Integer);
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 LastError

Usage Examples

1. Simple procedure

procedure PrintHello;
begin
  WriteLn('Hello from thread ', GetCurrentThreadId);
end;

begin
  GlobalThreadPool.Queue(@PrintHello);
  GlobalThreadPool.WaitForAll;
end;

2. Object method

Warning: do not free MyObject until after WaitForAll returns. Worker threads hold a reference to the object's method and will crash if the object is freed while they are still running.

type
  TMyClass = class
    procedure ProcessData;
  end;

procedure TMyClass.ProcessData;
begin
  WriteLn('Processing in thread ', GetCurrentThreadId);
end;

var
  MyObject: TMyClass;
begin
  MyObject := TMyClass.Create;
  try
    GlobalThreadPool.Queue(@MyObject.ProcessData);
    GlobalThreadPool.WaitForAll;  // must finish before Free below
  finally
    MyObject.Free;
  end;
end;

3. Indexed procedure (loop parallelism)

procedure ProcessItem(index: Integer);
begin
  WriteLn('Item ', index, ' in thread ', GetCurrentThreadId);
end;

var
  i: Integer;
begin
  for i := 0 to 9 do
    GlobalThreadPool.Queue(@ProcessItem, i);

  GlobalThreadPool.WaitForAll;
end;

4. Custom pool with error check

var
  Pool: TSimpleThreadPool;
begin
  Pool := TSimpleThreadPool.Create(4);
  try
    Pool.Queue(@RiskyProcedure);
    Pool.WaitForAll;

    if Pool.LastError <> '' then
      WriteLn('Task failed: ', Pool.LastError);
  finally
    Pool.Free;
  end;
end;

Common Pitfalls

Freeing an object before WaitForAll

// WRONG — worker thread may still be calling MyObject.ProcessData
MyObject := TMyClass.Create;
GlobalThreadPool.Queue(@MyObject.ProcessData);
MyObject.Free;  // access violation risk

// CORRECT
MyObject := TMyClass.Create;
try
  GlobalThreadPool.Queue(@MyObject.ProcessData);
  GlobalThreadPool.WaitForAll;
finally
  MyObject.Free;
end;

Calling Free on GlobalThreadPool

// WRONG — the unit's finalization block already does this
GlobalThreadPool.Free;  // double-free at program exit

// CORRECT — just use it; no manual cleanup needed
GlobalThreadPool.Queue(@MyProcedure);
GlobalThreadPool.WaitForAll;

Forgetting WaitForAll

// WRONG — program may exit before tasks finish
for i := 0 to 99 do
  GlobalThreadPool.Queue(@ProcessItem, i);
// results are incomplete or uninitialized here

// CORRECT
for i := 0 to 99 do
  GlobalThreadPool.Queue(@ProcessItem, i);
GlobalThreadPool.WaitForAll;
// results are now complete