Threads & parallelism

The coroutine scheduler gives cooperative concurrency on a single OS thread. This page covers the other axis: real OS threads and data-parallel loops that use every core. Both are libc-free — the runtime talks to the kernel's thread primitives directly.

These are advanced surfaces. Multi-threaded code compiled without --threadsafe shares unmanaged refcounts and will corrupt managed strings and dynamic arrays under contention. Build threaded programs with --threadsafe.

TThread — the palthreadobj unit

TThread is an FPC-style thread base class. Subclass it, override Execute, and the body runs on its own OS thread.

program worker;
{ compile with: ./pxx --threadsafe worker.pas worker }
uses palthread, palthreadobj;

type
  TAdder = class(TThread)
  public
    Sum: Int64;
  protected
    procedure Execute; override;
  end;

procedure TAdder.Execute;
var i: Integer;
begin
  Sum := 0;
  for i := 1 to 1000000 do
    Sum := Sum + i;            { runs on this thread's own OS thread }
end;

var t: TAdder;
begin
  t := TAdder.Create(False);   { False = start immediately }
  t.WaitFor;                   { block until Execute returns }
  writeln('sum = ', t.Sum);
  t.Free;                      { also Terminate+WaitFor if still running }
end.

To hand results back to the main thread safely, Synchronize(m) and Queue(m) marshal a TThreadMethod onto it. Bind the method to a variable first (m := @Self.SomeMethod;) — an inline Synchronize(@Self.SomeMethod) argument is not parsed yet — and have the main thread call CheckSynchronize periodically to run the queued work.

Surface

Member Effect
constructor Create(CreateSuspended: Boolean) Create the thread; False starts it at once, True waits for Start.
procedure Start Begin a suspended thread.
procedure Execute; virtual; abstract The thread body — override this.
procedure WaitFor Block the caller until the thread finishes.
procedure Terminate Set the cooperative Terminated flag; the body must observe it.
procedure Synchronize(m) / procedure Queue(m) Run a method on the main thread — blocking / fire-and-forget.
function ThreadID: Int64 The OS thread id.
property Terminated / Finished / Suspended Thread-state flags.
property FreeOnTerminate Self-Free when Execute returns.
property OnTerminate Method fired (on the main thread) when the thread ends.

Synchronize and Queue deliver work to the main thread; the main thread runs that work when it calls CheckSynchronize (or blocks in a runtime that pumps it). MainThreadID and CurrentThread identify the running thread.

parallel for — the palparallel unit

parallel for is a language-level statement that fans a counted loop across a libc-free worker pool. The compiler desugars it at parse time into a synthesised worker procedure plus a pool dispatch — there is no closure allocation.

program parsum;
{ compile with: ./pxx --threadsafe parsum.pas parsum }
uses palparallel;

const N = 100000;
var arr: array[0..N-1] of Integer;

procedure Run;
var i: Integer;
begin
  parallel for i := 0 to N-1 do
    arr[i] := i * 3;          { each iteration writes its own disjoint slot }
end;

begin
  Run;                        { must sit inside a routine, not the main body }
  writeln(arr[42]);
end.

Constraints in this version:

An optional policy clause tunes distribution (worker count, chunking); omit it for the default that fans the range evenly across the pool.

reduction and private

Two clauses sit between the range and do, and both exist because capture is by reference:

  parallel for i := 0 to N-1 reduction(+: total) private(scratch) do
  begin
    scratch := 0;
    while scratch < 3 do begin total := total + 1; scratch := scratch + 1; end;
  end;

Two things to know about private:

A variable may not appear in both clauses — reduction already gives it a private, plus the combine.

Scalars and AnsiString may be private. Arrays, records and classes may not yet; index a shared array by the loop variable instead, which needs no clause.

Next