REXX Language implementation
The concurrency namespace is the object layer over cREXX’s provider-neutral
channel instructions. It supplies bounded task pools, structured scopes,
canonical values, byte endpoints and explicit binary ownership. The language
chapter adds Level G task syntax on top; the classes themselves are available
at Level B.
The surface is initial on develop. Local-thread and isolated-process
providers are qualified on macOS in the current tree. Portable publication is
still subject to Linux and Windows conformance.
options levelb
import rxfnsb
import concurrency
A pool and a scope have different jobs:
.taskpool is a bounded execution provider and capacity policy;.taskscope owns child lifetime, cancellation, deadline and joining;.task is one submitted child; and.completion is an immutable terminal observation.One pool may serve several scopes. A scope is consumed once and cannot be reused. Close every scope before closing its pool. Ordinary tasks cannot detach from their scope.
taskpool (capacity) --> taskscope (lifetime) --> task --> completion
|
+--> task --> completion
The common transport layer has the same ownership shape:
channel (provider owner) --> channelrequest --> completion
Raw channel handles and request tickets are deliberately private. The wrapper objects prevent an integer from being mistaken for transferable authority.
Create a local worker-thread pool or an isolated worker-process pool:
local_pool = .taskpool.local(4, 64)
process_pool = .taskpool.process(2, 16)
The first argument is maximum worker capacity. The second is bounded admission
capacity. Provider type 1 is local execution and type 2 is process
isolation. Capacity permits overlap; it does not promise a particular
schedule.
Choose a scope policy and relative deadline:
scope = .taskscope.failfast(local_pool, 30000)
scope = .taskscope.collectall(local_pool, 30000)
failfast requests cancellation of unfinished siblings after the first
unsuccessful child. collectall lets the other children finish. Both account
for every accepted child. Timeout values are milliseconds: -1 means no
deadline or an indefinite wait, 0 is an immediate poll, and a positive value
is a bounded relative wait.
The most Rexx-like way to use these objects is through Level G task syntax:
options levelg comments_dash
import concurrency
adderwork: class implements .taskwork
_delta = .int
*: factory
arg delta = .int
_delta = delta
return
run: method = .channelvalue
arg request = .channelvalue, context = .taskcontext
if context.cancellation_requested() then return .channelvalue.null_value()
return .channelvalue.integer_value(request.as_integer() + _delta)
main: procedure = .int
pool = .taskpool.local(1, 4)
scope = .taskscope.collectall(pool, 60000)
target = task .adderwork(2)
child = scope.submit(target, .channelvalue.integer_value(40))
outcome = child.wait(-1)
if outcome.succeeded() then answer = outcome.value().as_integer()
else do
say outcome.error_code() outcome.message()
answer = -1
end
call scope.finish()
call pool.close()
return answer <> 42
exit main()
This exact pattern is compiled, assembled, linked and run in
concurrency_taskwork.crexx.
task .adderwork(2) is Level G syntax: the compiler, assembler and linker
create and seal the target descriptor. Application code must not call
.tasktarget.binding() with invented bytes.
The main scope operations are:
| Operation | Meaning |
|---|---|
submit(target, request) |
submit one .channelvalue application request |
next(milliseconds) |
observe the next child in completion order |
join() |
return all child completions in submission order |
cancel(reason) |
request cancellation of unfinished children |
finish() |
join, close and raise TASK_FAILURE on child failure |
abort(reason) |
cancel, join and close during abnormal controller exit |
next() may return an unavailable sentinel with available() = 0. That is a
timed or nonblocking miss, not a child completion, and it never appears in
join().
.taskscope.ask() is reserved for future single-owner services and currently
signals unsupported status 19. .taskpool.queued() and .running() are also
reserved and signal 19; they do not return guessed telemetry.
.taskwork is the advanced runnable contract below ordinary typed task calls:
run: method = .channelvalue
arg request = .channelvalue, context = .taskcontext
A factory target such as task .adderwork(2) creates a worker-owned class
instance. The application request is separate from the factory arguments.
The current factory-target lowering accepts .boolean, .int, .string,
.binary and direct .channelvalue arguments.
The context reports:
timeout_remaining(): remaining milliseconds, or -1 without a deadline;cancellation_requested(): cooperative cancellation state; andtrace_identity(): stable invocation identity.endpoint(reference) adapts a transferable type-4 provider reference to a
worker-local .byteendpoint. Use it inside .taskwork.run() when a task must
read or write an endpoint owned by its controller; ordinary controller code
can use .byteendpoint.from_reference() directly.
.taskarguments and the submit_arguments() / result_*() methods are
exposed compiler-lowering bridges. They are documented so generated code is
auditable, but ordinary programs should use typed task calls or
submit(target, request).
Always test available() before treating an observation as terminal, then use
succeeded(), value(), error_code(), message() and details() rather
than guessing from a message string.
| State | Meaning |
|---|---|
0 |
unavailable observation sentinel |
1 |
succeeded |
2 |
failed |
3 |
cancelled |
4 |
deadline exceeded |
5 |
rejected or target not found |
6 |
endpoint closed or provider shutdown |
7 |
transport lost before execution |
8 |
outcome unknown after transport loss |
9 |
isolated task killed |
State is provider-neutral terminal classification. error_code() carries the
more specific operation/provider status. value() is valid only when
has_value() is true.
.channelvalue is the only ordinary value document accepted at a provider
boundary. It can represent null, boolean, integer, float, decimal, string,
binary, arrays and named versioned records. The receiver owns a validated new
value; it does not receive a pointer to the sender’s object.
names = .string[]
values = .channelvalue[]
names[1] = "name"
values[1] = .channelvalue.string_value("Ada")
names[2] = "score"
values[2] = .channelvalue.integer_value(42)
record = .channelvalue.record_value("example.result", 1, names, values)
say record.schema() record.version()
say record.field("score").as_integer()
Record field names must be unique. Construction writes canonical byte-name
order, so equivalent records encode identically. Accessors validate the value
kind: for example as_integer() does not coerce a string.
.channelcodec is the explicit application codec interface:
schema: method = .string
version: method = .int
encode: method = .channelvalue
arg item = .object
decode: method = .object
arg item = .channelvalue
There is no ambient runtime codec registry. Level G transferable concrete
classes instead use their statically resolved to_channel() and
from_channel() members.
Most programs use pools, scopes and .byteendpoint rather than opening a raw
.channel. The channel object directly exposes the provider lifecycle:
channel = .channel.open(provider_type, required_capabilities, configuration)
request = channel.start(envelope, admission_wait)
outcome = request.wait(completion_wait)
call channel.close(1) /* drain */
Close mode 1 drains accepted work; mode 2 requests cancellation. A channel
owns its requests, so cancellation with a request from another channel is an
error.
Provider type 4 supplies reusable bounded byte endpoints. This complete
Level B example writes and reads through one duplex endpoint:
options levelb
import rxfnsb
import concurrency
endpoint = .byteendpoint.memory(3, 32)
write_request = endpoint.start_write("hello" as .binary, -1)
write_result = write_request.wait(-1)
if write_result.succeeded() = 0 then exit 1
read_request = endpoint.start_read(5, -1)
read_result = read_request.wait(-1)
if read_result.succeeded() = 0 then exit 2
if read_result.value().as_binary() as .string <> "hello" then exit 3
call endpoint.half_close(2)
call endpoint.close()
exit 0
Endpoint direction is 1 readable, 2 writable or 3 duplex. Memory
capacity is a real backpressure bound. A transferable provider reference can
be adapted in another execution with from_reference() or, for an exact
encoded 92-byte reference, from_encoded_reference(). Close every adapter
when finished.
.transferbuffer makes binary ownership transitions visible:
buffer = .transferbuffer.copy_from("answer=41" as .binary)
call buffer.write(7, "42" as .binary)
snapshot = buffer.seal()
say snapshot.as_binary() as .string
The states are:
move_value() or seal();seal() calls
return the same stable immutable snapshot.A transfer-buffer seal is not encryption, authentication or a checksum. It only prevents later writes through that buffer. It is unrelated to the sealed task-binding integrity descriptor.
.serviceref records the intended logical identity shape for a future
single-owner service. There is no concrete public service, no working
.taskscope.ask() path and no typed service proxy today. Provider type 3
(open host) and the public provider-plugin ABI are also reserved.
For task-call syntax and more approachable examples, see Concurrent programming. For the formal Level G rules, see the Concurrency language reference.