Your other 15 cores were unemployed. Until today.New feature: Futures - async & awaitOn by default in
{$mode unleashed}. Outside:
{$modeswitch asyncawait}.
Docs: docs/async-await.mdasync runs a call or a block on a fresh worker thread and hands back a
future;
await blocks until the worker finishes and reads its result. This is the
std::async model from C++,
not the C# one - no function coloring, no event loop, no scheduler. One
async is one thread, one
await is one join. Simple to reason about.
program async_demo;
{$mode unleashed}
uses SysUtils;
function Add(a, b: Integer): Integer;
begin
sleep(300); // pretend this is expensive
result := a + b;
end;
var a: Integer;
begin
a := 2;
var sum := async Add(a, 3); // spawns a worker; a is snapshotted as 2, type inferred: future of Integer
a := 100; // too late - does not affect the call
writeln('main thread keeps going...');
writeln('sum = ', await sum); // sum = 5
readln;
end.
A
future of T is a first-class, reference-counted value: store it, pass it around, return it from a function - it stays alive as long as anything holds it. Spawn without keeping it and the work still runs (fire and forget):
program async_future;
{$mode unleashed}
uses SysUtils;
function FetchName: string;
begin
sleep(50); // pretend it's a network call
result := 'Bob';
end;
procedure LogToFile(const s: string);
begin
writeln('log: ', s);
end;
function StartFetch: future of string;
begin
result := async FetchName; // the future outlives this function
end;
begin
async LogToFile('done'); // fire and forget - nobody waits
var f := StartFetch; // a future returned from a function
writeln('hi ', await f); // hi Bob
readln;
end.
Two spawn formsCall form async SomeCall(args) evaluates the arguments
now, snapshots them by value, and runs the routine on the worker from those snapshots (that's why
sum above is 5, not 103).
Block form async begin ... end captures referenced locals
by reference on the heap, so the worker sees later mutations and the future may safely outlive the spawning routine.
Controlling the workerThe future carries a small control surface:
Cancel,
Cancelled,
Done (never blocks - poll it for progress),
ThreadID (plugs straight into the RTL thread API). Cancellation is cooperative - inside a block the flag is visible as a read-only
Cancelled boolean:
program async_cancel;
{$mode unleashed}
uses SysUtils;
begin
var ticks := 0;
var h := async begin // block form: captures locals by reference
while not Cancelled do begin // cooperative cancel flag, visible in the block
inc(ticks);
sleep(10);
end;
end;
sleep(100);
writeln('running: ', not h.Done); // running: TRUE
h.Cancel; // ask the worker to stop
await h;
writeln('done: ', h.Done, ', ticked: ', ticks > 0); // done: TRUE, ticked: TRUE
readln;
end.
Exceptions cross the thread boundaryAn exception raised on the worker is captured and re-raised on the caller at the first
await - a background failure surfaces as an ordinary exception, exactly where you read the result:
program async_exceptions;
{$mode unleashed}
uses SysUtils;
begin
var bad := async begin
raise Exception.Create('boom');
end;
try
await bad; // worker exception re-raised here
except
on E: Exception do writeln('caught: ', E.Message); // caught: boom
end;
readln;
end.
Notes- await is repeatable - the result is cached in the future, only the first await actually waits. Reading a future's value without await is a compile error.
- The call form rejects var/out arguments (a by-value snapshot would silently drop the writes) and nested routines (they outlive the parent frame) - both with dedicated errors pointing at the block form.
- Shared mutable state is your responsibility: snapshots protect the call form's arguments, but block captures and object fields are shared - synchronize yourself (lock works).
- On Unix add cthreads as the first unit, as for any threaded FPC program; on Windows it works out of the box.
New feature: parallel forOn by default in
{$mode unleashed}. Outside:
{$modeswitch parallelfor}.
Docs: docs/parallelfor.mdfor parallel var i := lo to hi do runs the loop body across a pool of worker threads instead of one after another. Iterations are handed out dynamically through an atomic counter (uneven work balances itself), the loop is a barrier - control continues only when every iteration has finished - and the body is the same statement you'd write in a classic
for:
program pfor_demo;
{$mode unleashed}
begin
var total := 0;
for parallel var i := 1 to 10000 do
InterlockedExchangeAdd(total, i); // shared variable -> atomic
writeln(total); // 50005000, every time
readln;
end.
What it buys you - counting primes up to 4 million, same
IsPrime, one line changed:
program pfor_primes;
{$mode unleashed}
uses SysUtils;
function IsPrime(n: Integer): Boolean;
var d: Integer;
begin
if n < 2 then exit(false);
d := 2;
while d * d <= n do begin
if n mod d = 0 then exit(false);
inc(d);
end;
result := true;
end;
const N = 4000000;
var
count: Integer;
t0: QWord;
begin
count := 0;
t0 := GetTickCount64;
for var i := 2 to N do
if IsPrime(i) then Inc(count);
writeln('sequential: ', count, ' primes, ', GetTickCount64 - t0, ' ms');
count := 0;
t0 := GetTickCount64;
for parallel var i := 2 to N do
if IsPrime(i) then InterlockedIncrement(count);
writeln('parallel: ', count, ' primes, ', GetTickCount64 - t0, ' ms');
readln;
end.
// sequential: 283146 primes, 734 ms
// parallel: 283146 primes, 109 ms
The header composesPool size,
downto,
step and
chunk (how many iterations one atomic grab claims - keeps cheap bodies from paying a contended atomic per index) all fit in the usual places:
program pfor_forms;
{$mode unleashed}
begin
var total := 0;
for parallel var i := 1 to 1000 do // pool = CPU count
InterlockedExchangeAdd(total, i); // +500500
for parallel(4) var i := 100 downto 1 do // explicit 4 workers
InterlockedExchangeAdd(total, i); // +5050
for parallel var i := 1 to 999 step 2 chunk 64 do // 1,3,5,... claimed 64 at a time
InterlockedExchangeAdd(total, i); // +250000
writeln(total); // 755550
readln;
end.
Without
chunk the block size defaults to about four grabs per worker;
parallel(1) degenerates to a plain sequential loop, and the calling thread is always one of the workers - it doesn't sit idle.
WorkerIndex / WorkerCountInside the body two implicit read-only locals identify the executing worker - the clean way to keep per-worker private state (a partial sum, a scratch buffer) with no locking at all:
program pfor_worker;
{$mode unleashed}
var
acc: array[0..3] of Integer; // one private slot per worker
total: Int64;
w: Integer;
begin
for parallel(4) var i := 1 to 100000 do
acc[WorkerIndex] := acc[WorkerIndex] + i; // private slot - no atomics needed
total := 0;
for w := 0 to 3 do
total := total + acc[w]; // combine after the barrier
writeln(total); // 5000050000
readln;
end.
Notes- The loop variable must be declared inline (for parallel var i := ...) - each worker owns its copy; a shared pre-existing counter is rejected.
- An exception in a body is caught, the pool joins, and the first one is re-raised on the calling thread - a fault surfaces at the loop, not as a crash on a helper thread.
- continue works as usual; break cancels cooperatively (no new iterations start, running ones finish). exit and goto out of the body are compile errors.
- Nested parallel loops don't oversubscribe: an inner for parallel on a worker runs sequentially unless you opt in with an explicit parallel(N).
- parallel is context-sensitive - only recognized between for and the header, so existing code using it as an identifier keeps compiling. Same for chunk.
- On Unix add cthreads as the first unit (the compiler hints about it); on Windows it works out of the box.