Documentation/Chapters

Channels, SELECT and timeouts

On this page

A channel coordinates tasks by exchanging typed messages. Use messages instead of trying to share arbitrary writable variables. The channel owns its queue and wait registrations; sender and receiver capabilities authorize specific operations without exposing that internal storage.

Send a sequence and close it

DIM jobs AS CHANNEL OF INTEGER = Channel(2)
producer = SPAWN Produce(Sender(jobs))
DO
    IF LET value = Receive(Receiver(jobs)) THEN
        PRINT value
    ELSE
        EXIT DO
    END IF
LOOP
Join(producer)
SUB Produce(output AS SENDER OF INTEGER) FAILS ChannelClosed
    Send(output, 10)
    Send(output, 20)
    Close(output)
END SUB

Capacity 2 permits two buffered messages. Capacity zero is a rendezvous: Send waits until a receiver accepts the message. A full buffer parks senders; an empty open channel parks receivers. These operations wait through the scheduler rather than spinning or requiring an OS thread for each wait.

Messages have independent-value semantics. Changing a source list after sending cannot alter the delivered message. Endpoints cannot escape their channel owner’s scope or become arbitrary long-lived stored capabilities. The supported message subset uses copyable data; resource-message transfer is not a general channel API.

Close ends sending. It does not discard accepted buffered messages. Receivers get SOME(message) until drained, then NONE. A nested optional message NONE is delivered as SOME(NONE). Sending after closure reports ChannelClosed. Closing one borrowed capability is a deliberate protocol operation, not a side effect of merely ending a local parameter’s scope.

Wait for one of several events

DIM notices AS CHANNEL OF STRING = Channel(1)
Send(Sender(notices), "Ready")
Close(Sender(notices))
SELECT
CASE SOME(message) = RECEIVE(Receiver(notices))
    PRINT message
CASE NONE = RECEIVE(Receiver(notices))
    PRINT "Closed"
CASE AFTER Seconds(1)
    PRINT "Timed out"
END SELECT

Scheduler SELECT commits exactly one case. Each receiver requires paired SOME and NONE cases with the same expression spelling, so the operation covers both message and closure. Current operands are named receiver parameters or Receiver(namedOwner). Case message bindings are read-only and local to that case. SELECT SEND and scheduler CASE ELSE are not implemented.

The runtime unregisters losing waits without consuming their messages. Priority rotates across repeated entries to a SELECT site when multiple cases are ready. A permanently closed receiver remains ready; stop selecting it when it is no longer useful. Fairness cannot make progress if your program stops entering the selection or performs work that never reaches a scheduling boundary.

Observe completion with a deadline

worker = SPAWN Work()
SELECT
CASE COMPLETED(worker)
    PRINT "Ready to join"
CASE AFTER Seconds(1)
    Cancel(worker)
END SELECT
IF Join(worker) THEN PRINT "Joined"
SUB Work()
    PRINT "Working"
END SUB

COMPLETED means the task is ready to observe, not that it succeeded. Always Join to handle retained success, failure or cancellation. A timeout winning the SELECT does not erase a child error.

AFTER takes a typed Duration such as Milliseconds(100). Each entry starts a fresh relative timeout; re-entering it is not a fixed periodic cadence. Nonpositive durations are immediately ready. Unrepresentable delays or absolute deadlines raise DateTimeError before registering any waits, so functions using AFTER must catch or declare it. Operand evaluation counts against the new timeout.

Cancellation before commitment consumes no message and removes waits. After a delivery commits, cancellation may prevent the body from observing it; the committed value is still destroyed exactly once rather than put back into the channel. Design protocols around commitment and acknowledgement, rather than assuming cancellation rewinds already accepted work.

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.