Part V — Concurrency & iteration

Fibers (stackful coroutines), generators built on them, and the Iterate mixin that turns a single each: into a full collection API — then the concurrency system proper: detached Tasks on one cooperative scheduler, the structured Async helpers, CSP Channels, and true parallelism with Worker isolates.

Nav: Foundations · Blocks & control · Objects · Patterns & errors · Concurrency & iteration · Networking & the web · Types · Tooling · Library & reference · Appendices


16. Fibers & generators

Rules

var f = Fiber.new:{ Fiber.yield:1; Fiber.yield:2; 'done' }
f.resume        "* -> 1
f.resume        "* -> 2
f.resume        "* -> 'done'
f.done?         "* -> true
var evens = Generator.from:{ var n = 0; { true }.whileDo:{ ^> n; n = n + 2 } }
evens.take:4    "* -> #(0 2 4 6)

The first resume runs to the first yield; the third returns the block's final value. evens is an infinite source, consumed lazily by take:.

⚠ Gotcha — resuming a finished or failed fiber throws. Once status is 'done' or 'failed', resume raises a FiberError. So does Fiber.yield: called outside any fiber, and a fiber attempting to resume itself. Check alive?/done? before resuming in a loop.


17. The iteration protocol

Rules

  MyThing <- { …
      .mix:Iterate
      each: -> { |b| … b.valueWithSelfOrArg:element … }   "* call b once per element
  }

Combinators provided by Iterate

Transform: collect:, select:, reject:, flatten, groupBy:, partition:, uniq, zip:, reverse, sort / sort:. Reduce/query: reduce:, reduce:into:, detect:, all?:, any? / any?:, none?:, count / count:, contains?:, sum / sum:, min / max / min: / max:. Access: firstfifth, last, nth:, take:, drop:, list, join:, iterator. Lazy: lazyCollect:, lazySelect:.

MyRange <- { |@start @end|
    .mix:Iterate
    each: -> { |b| var i = @start; { i < @end }.whileDo:{ b.valueWithSelfOrArg:i; i = i + 1 } }
}

var r = MyRange.new:{ start = 0; end = 5 }
r.collect:{ |n| n * n }        "* -> #(0 1 4 9 16)
r.select:{ |n| n > 2 }         "* -> #(3 4)

Every combinator here — collect:, select:, and the rest — comes from that one each:.

⚠ Gotcha — combinators return materialized lists; use the lazy* forms for infinite or expensive sources. collect:/select: walk the whole collection eagerly. Over an infinite Generator, use lazyCollect:/lazySelect: and finish with take:; otherwise iteration never terminates.


18. Tasks & the cooperative scheduler

A fiber (§16) is a coroutine you drive with resume. A task is scheduled for you: the VM interleaves every spawned task (the top level of the program is itself a task) on one cooperative scheduler, overlapping their waits.

Rules

var t = Task.spawn:{ 21 * 2 };
t.join    "* -> 42

A failed task holds its error until someone joins it:

var t = Task.spawn:{ ValueError.throw:'boom' };
{ t.join }.catch:{ |e:ValueError| 'caught: ' + e.message }    "* -> caught: boom

Cancellation is a request, honored at the task's next parking point. Its finally: blocks run; its catch: handlers do not see the cancellation:

var log = #();
var t = Task.spawn:{ { Async.sleep:50 }.catch:{ |e| log.add:'caught' } finally:{ log.add:'cleanup' } };
Async.sleep:5;        "* let the task start and park inside its sleep
t.cancel;
Async.sleep:100;      "* give the cancellation time to land
#( t.status log )     "* -> #(cancelled #(cleanup))

Parking parks the task, not the VM

This is the load-bearing semantic of the whole system. A read that would "block" — stdin, a socket, a sleep — hands control back to the scheduler, which runs whatever else is ready. Here the main task waits on stdin while a spawned ticker keeps ticking (illustrative — it needs a terminal):

"* tick.qn — the ticker runs while the main task waits on stdin
var ticker = Task.spawn:{
    (0..3).each:{ |i| ('tick ' + i.s).print; Async.sleep:200 }
};
var line = [IO]Stdin.readLine;    "* parks THIS task only
ticker.join;
('read: ' + line).print
$ qn tick.qn        (typing "hello" about half a second in)
tick 0
tick 1
tick 2
read: hello

The same holds for every parking point. A deterministic, runnable version — join is the park, and the spawned task runs to completion while the main task waits:

var log = #();
var t = Task.spawn:{ (0..3).each:{ |i| log.add:i } };
t.join;    "* main parks here; the spawned task runs
log        "* -> #(0 1 2)

⚠ Gotcha — spawning queues; only joining guarantees. Task.spawn: queues the task; it gets its first turn at the spawner's next parking point or scheduler boundary (near-immediately, even if the spawner is compute-bound — CPU-bound tasks round-robin rather than starving each other). But the process exits when the main program finishes, abandoning whatever is still queued or running — join, Async.gather:, or a channel handshake is what guarantees completion. The one thing that still monopolizes the thread is a single long-running native call: nothing preempts inside one send.


19. Async — sleep, gather, timeout

Structured helpers over the task scheduler: Task.spawn: is fire-and-forget; these wait for their work.

Rules

Async.gather:#( { Async.sleep:2; 'a' } { 'b' } )    "* -> #(a b)

'b' finishes first, but results keep the input order — gather is about overlapping waits, not racing.

Async.timeout:50 do:{ 42 }                                       "* -> 42
Async.timeout:5 do:{ Async.sleep:200; 1 } onCancel:{ 'late' }    "* -> late
{ Async.timeout:5 do:{ Async.sleep:200 } }
    .catch:{ |e:TimeoutError| e.message }    "* -> operation timed out after 5ms
var ts = #( { Async.sleep:1; 1 } { 2 } ).collect:{ |b| Task.spawn:b };
Async.joinAll:ts    "* -> #(1 2)

⚠ Gotcha — gather: takes blocks, joinAll: takes handles. Passing task handles to gather: (or blocks to joinAll:) is a type error. And because a deadline cancels the block it wraps, don't put must-complete side effects inside timeout:do: without a finally:.


20. Channels

CSP-style message passing between tasks: instead of sharing a structure and coordinating around parking points, hand values from task to task.

Rules

var ch = Channel.new;
Task.spawn:{ ch.send:42 };
ch.receive    "* -> 42

The main task parks in receive; that yields to the spawned sender, whose send: completes the rendezvous. A buffered channel decouples the two sides:

var ch = Channel.buffered:2;
ch.send:1; ch.send:2; ch.close;
#( ch.receive ch.receive )    "* -> #(1 2)

Producer/consumer, with close as the end-of-stream signal:

var ch = Channel.buffered:8;
Task.spawn:{ (0..5).each:{ |i| ch.send:(i * i) }; ch.close };
var got = #();
ch.each:{ |v| got.add:v };
got    "* -> #(0 1 4 9 16)

⚠ Gotcha — the deadlock error usually means "nobody on the other end". A receive with no task that will ever send (or an unbuffered/full send: with no task that will ever receive) can never be woken; once every task is in that state the scheduler reports it rather than hanging silently:

$ qn -e 'Channel.new.receive'
deadlock: every task is parked with no I/O in flight (e.g. a receive with no sender, or a join cycle); the program cannot make progress

Spawn the other side before parking (as in the examples above), buffer the channel, or close it when the producer is done.


21. Workers, Parallel & Plan — true parallelism

Everything so far shares one OS thread and one heap. For real parallelism, Quoin uses isolates: a Worker is a fresh VM on its own OS thread (or child process) connected to its parent by message lanes — no shared state, parallelism by message passing. This section is an overview; run qn doc for the generated per-class API reference.

Rules

Round trip through a block worker's lanes (the block is the whole worker program; receive parks the parent task, like any wait):

var w = Worker.start:{ var n = Worker.receive; Worker.send:(n * 2); 'done' };
w.send:21;
w.receive    "* -> 42

The parallel combinators keep collect:'s contract — this input is far below Parallel.minItems, so it runs serially, with the same result either way:

#(1 2 3).parallelCollect:{ |x| x * 10 }    "* -> #(10 20 30)

A Plan mixes in-VM tasks and isolates in one awaited shape:

(Plan.all:#( (Plan.task:{ 1 + 1 }) (Plan.thread:{ 2 + 2 }) )).await    "* -> #(2 4)
(Plan.all:#( (Plan.task:{ 1 }) (Plan.task:{ 'boom'.throw }) ) onError:'collect').await
    "* -> #(#{'ok': 1} #{'err': 'boom'})

Hosting a class as a service (illustrative — it needs the unit file):

var index = WorkerService.host:'search/index.qn' class:'SearchIndex';
index.add:doc;                     "* an ordinary send — runs inside the isolate
var hits = index.query:'quoin';
index.serviceStop

⚠ Gotcha — messages are copies. A List sent to a worker (or received from one) is deep-copied at the boundary: mutating it on one side never affects the other. Isolation is the point — design worker protocols around values passed through the lanes, not shared structures.


Next: Part VI — Networking & the web.