Contexts and Threads
A Context gives a task its own Scope, Error state, and Match state. An
isolated Context also gives it a child canonical-value pool, which its
Strings and Lists share. Closing the Context restores the state that was
active before it opened and reclaims everything that was not exported.
If opening a Context fails, the parts already opened are closed before the
failure reaches an outer catch. A failed open does not change
Context.current() or leave a pool active.
A Thread runs a callback in an isolated Context on a native worker thread.
The worker may read parent values and may access shared mutable objects. The
program must keep those objects alive and coordinate any access that could
race.
Use a Context for bounded work
Context.open and Context.open_named use the current canonical-value
pool. Use them when the work needs fresh Scope, Error, and Match state
and its Strings and Lists should live as long as the caller’s.
Context.open_isolated and Context.open_isolated_named add a child pool.
New temporary Strings and Lists are then reclaimed when the Context
closes. Values already in a parent pool stay visible and keep their
identity. The name given to a _named form appears only in Scope leak
diagnostics.
The following program builds a cyclic mutable result around a private String,
exports it, and closes the Context. Context.export moves the Array and
Map storage into the caller’s Scope, copies the private immutable values
into the caller’s pool, and preserves the cycle:
#include <stdio.h>
int main(void) {
Context input = Context.open_isolated_named("input file");
String temporary = String.new("temporary text");
Array values = [];
values.push(temporary);
Map result = {};
result[<values>] = values;
result[<self>] = result;
result = input.export(result);
input.close();
printf("%s\n", result[<values>].array()[0].string());
return 0;
}
Always use the value returned by Context.export. Mutable Arrays, Maps,
Blocks, Bytes, Buffers, and wide scalar boxes retain their identity when their
storage can be moved. Private Strings, Atoms, and Lists are canonicalized
in the destination pool, so their pointers may change. Export walks nested
Arrays, Maps, and Lists, including repeated references and Array/Map
cycles. A registered custom value can provide its own Context exporter.
void exports as void.
The seven packed Arrays from typed-array.x and the four packed Maps
from typed-map.x follow the same rule. A container owned by the source
Context keeps its pointer, and its header and backing storage move
together. A borrowed container is returned unchanged. The numeric families
keep their native entries and continue growing in the destination Scope.
ArrayString canonicalizes its elements, MapStringString its keys and
values, and MapStringInt its keys, in the destination pool before the
container moves, so their String pointers may change. If two keys become
the same destination String, the entry visited later in source bucket
order replaces the earlier one. Map traversal order is unspecified.
A custom protocol Var(T) participant may implement T.export_context to
move its mutable storage into another Context. The member is optional, like
the other Var methods. Its implementation recursively exports nested values
and moves its owned allocations through the Context helpers; application code
still calls Context.export on the complete value.
Export covers values allocated through the active Context and values
inherited or borrowed from its caller. Code that manually creates an unrelated
Scope or immutable pool must manage that lifetime itself; returning such a
value as though the Context owned it is undefined behavior.
The complete program is in examples/power/contexts.x.
Pass shared values to a worker
Thread.start copies the input structure’s bytes. It does not copy anything
reached through pointers in that structure. A pointer, String, List, Map,
or other handle therefore refers to the same parent object in the worker. The
copied bytes have max_align_t alignment, matching ordinary allocation; do not
pass a structure that requires extended alignment beyond that.
Parent immutable Strings and Lists can be read directly. Shared mutable
objects can also be read or changed, but normal C data-race rules apply: a read
is safe only when no thread can write at the same time, and coordinated writes
need a Mutex or another suitable mechanism. Every referenced parent object,
including the Mutex, must remain alive until all workers using it have
joined.
typedef struct Work {
int iterations;
int *counter;
Mutex mutex;
} Work;
static Var count(const void *input, size_t input_size) {
const Work *work = input;
if (input_size != sizeof(Work))
raise %(bad-arg (operation "count worker"));
for (int i = 0; i < work.iterations; i++) {
$lock(work.mutex) (*work.counter)++;
}
return work.iterations;
}
int main(void) {
int counter = 0;
Mutex mutex = Mutex.new();
Work work = { 1000, &counter, mutex };
Thread first = Thread.start(count, &work, sizeof(work));
Thread second = Thread.start(count, &work, sizeof(work));
first.join();
second.join();
first.free();
second.free();
mutex.free();
return counter == 2000 ? 0 : 1;
}
Each worker gets a private canonical-value pool, so its temporary
Strings and Lists are not kept for the life of the process. The
callback returns one Var. Thread.join waits for the worker and exports
that result into the joining Scope and active pool. Until the join, the
result stays in the sealed worker pool, even after the callback finishes.
Once the native join succeeds, the Thread consumes that storage, even if
exporting the result raises. A failed export cannot be retried. Call
Thread.free after Thread.join returns or after catching an Error
raised by the join.
A Thread cannot be detached or cancelled. Join every started Thread
before the process exits. If a worker is
still unjoined at shutdown, even one whose callback has finished, the
runtime prints a message and aborts. The join reclaims the worker’s result
Scope and pool.
A worker uses the starting thread’s Error policy. A cause whose policy is
<collect>, <log>, or <ignore> returns to its raise inside the
callback, as it does on the starting thread. A cause whose policy is
<abort> leaves the callback. That includes every shared cause and every
code that no policy names. Thread.join then raises <join-fail> with the
worker’s captured errors, and that raise does not return to the joiner.
So Thread.join either returns the worker’s exported result or transfers.
A callback that returns void joins as void.
Captured errors stay in the sealed worker storage and enter the joining
pool only during Thread.join. A worker Error whose policy is <log>
also passes through the global Logger error handler. Logger serializes
its sinks. A memory sink copies worker Strings and Lists into the pool
captured when its Logger was created, so logged entries stay valid after
the worker exits.
The more complete examples/power/threads.x uses two workers, parent
Strings, private temporary Lists and Strings, a shared counter protected
by a Mutex, joined Map results, and a global memory Logger sink.
All type and protocol registration must finish before the first successful
Thread.start. Starting native execution freezes descriptor declaration.
Boxing is not frozen: a worker may box the first value of a declared class,
and every thread then reads that class’s complete row.
Thread.start also turns on locking for the canonical-value pools. A process
that has never started a worker does not lock them. Create native threads
through Thread; one made by calling pthread_create directly can share
interned values with no lock protecting them.