Skip to content

23 · Concurrency

Channels

CSP-style message passing: channel(N), send/recv, for-in, bounded channels, and backpressure.

On this page

Channels are the idiomatic way to communicate between tasks in Zolo. channel(N) creates a channel with a buffer capacity of N. ch.send(v) enqueues a value; ch.recv() removes one and blocks the caller while the channel is empty — which is why consumers always need to run inside spawn { ... }.

recv returns an Option: use .is_some() / .unwrap() to inspect it, or .unwrap_or(default) to supply a fallback value. When the channel closes and the buffer is empty, recv returns nil (empty option).

Rendezvous channel (channel(0)) with two spawns: producer closes, consumer checks is_some and is_none.

09-channels-basic.zolo
Playground
// Feature: channels — typed CSP-style message passing
// Syntax: `channel(buffer_size)` creates one. `ch.send(v)` enqueues,
// `ch.recv()` dequeues. `recv` returns an `Option` — use `is_some` /
// `unwrap` (or `for x in ch { ... }` which strips Option for you).
// `recv` blocks the caller, so it must run inside a coroutine.
// When to use: producer/consumer pipelines, bounded queues, fan-out
// of work across spawned tasks.

let ch = channel(0)
scope {
    spawn {
        ch.send(10)
        ch.send(20)
        ch.close()
    }
    spawn {
        let a = ch.recv()
        let b = ch.recv()
        let c = ch.recv()
        print(a.is_some())   // expected: true
        print(a.unwrap())    // expected: 10
        print(b.is_some())   // expected: true
        print(b.unwrap())    // expected: 20
        print(c.is_none())   // expected: true   (channel closed, drained)
    }
}

The for x in ch { ... } pattern drains the channel until it closes, automatically unwrapping the Option. It is the cleanest way to consume a message stream.

Producer sends three values and closes; consumer uses for x in ch and prints each one.

10-channels-for-in.zolo
Playground
// Feature: receive loop — `for x in ch` until close

// Syntax: `for x in ch { ... }` calls recv internally and exits when

// the channel is closed. The body runs once per received value.

// When to use: streaming consumers, log readers, event drainers —

// anywhere "process every message until the producer stops".


let ch = channel(0)
scope {
  spawn {
    ch.send(1)
    ch.send(2)
    ch.send(3)
    ch.close()
  }

  spawn {
    for x in ch {
      print(x)
    }
  }
}

print("done")
// expected:

//   1

//   2

//   3

//   done

channel(N) with N >= 1 buffers up to N values before applying backpressure: the producer blocks on send while the buffer is full. This smooths bursts without unbounded memory growth.

Buffer of size 2: the third send waits until the consumer frees a slot.

11-channels-bounded.zolo
Playground
// Feature: bounded channels — backpressure built in
// Syntax: `channel(N)` reserves a buffer of size N. Sends past
// capacity yield until the consumer drains a slot.
// When to use: rate-limit producers, prevent memory blowups in
// pipelines, smooth bursty workloads with a fixed-size queue.

let ch = channel(2)
scope {
    spawn {
        ch.send("a")
        ch.send("b")
        // The buffer is full; this third send waits until the consumer
        // recvs at least once.
        ch.send("c")
        ch.close()
    }
    spawn {
        print(ch.recv().unwrap())     // expected: a
        print(ch.recv().unwrap())     // expected: b
        print(ch.recv().unwrap())     // expected: c
        print(ch.recv().is_none())    // expected: true
    }
}

Challenge

Modify the basic example to use channel(4) instead of channel(0) and observe how the producer advances without waiting for the consumer.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br