Channels - Message Passing Between Tasks¶
Channels provide safe communication between concurrent tasks. Instead of sharing memory directly (like Mutex), channels let tasks send messages to each other.
Creating a Channel¶
Use channel_new(capacity) to create a buffered channel:
The capacity determines how many messages can be buffered before send blocks.
Sender and Receiver¶
Channels split into two handles: - Sender: For sending values into the channel - Receiver: For receiving values from the channel
let ch = channel_new(10)
# Get the sender and receiver handles
let sender = ch.sender()
let receiver = ch.receiver()
Why Split?¶
This enables: 1. Ownership tracking: Know who can send vs receive 2. Multiple producers: Clone senders for many-to-one patterns 3. Single consumer: Typically one receiver (MPSC pattern)
Sending Values¶
Use send(value) to send a value:
let sender = ch.sender()
# Send returns bool: true if successful, false if channel closed
let ok = sender.send(42)
print(ok) # true
Blocking Behavior¶
send()blocks if the buffer is fulltry_send()returns immediately (non-blocking)
Receiving Values¶
Use recv() to receive a value:
let receiver = ch.receiver()
# recv returns Option<T>: Some(value) or Nothing if closed
let msg = match receiver.recv():
Option.Some(value): "Received: " + str(value)
Option.Nothing: "Channel closed"
print(msg)
Blocking Behavior¶
recv()blocks if the buffer is emptytry_recv()returns immediately (non-blocking)
# Non-blocking receive. Each arm is a single expression, so a branch that needs
# several statements calls a function.
let handled = match receiver.try_recv():
Option.Some(value): process(value)
Option.Nothing: nothing_available()
Closing a Channel¶
Close a channel to signal no more values will be sent:
After closing:
- send() returns false
- recv() returns Nothing once the buffer is empty
Complete Example¶
def main() -> int:
# Create a channel
let ch = channel_new(10)
let sender = ch.sender()
let receiver = ch.receiver()
# Send some values
sender.send(1)
sender.send(2)
sender.send(3)
# Receive and print
let v1 = receiver.try_recv() # Some(1)
let v2 = receiver.try_recv() # Some(2)
let v3 = receiver.try_recv() # Some(3)
let v4 = receiver.try_recv() # Nothing (empty)
# Close the channel
ch.close()
return 0
Channel Patterns¶
Producer-Consumer¶
# Producer task
async def producer(sender: Sender):
for i in range(100):
sender.send(i)
# Sender dropped when function returns
# Consumer task.
# A match arm is a single expression, so the Option is tested with if instead —
# `break` is a statement and cannot appear in an arm.
async def consumer(receiver: Receiver):
while true:
let msg = receiver.recv()
if msg.is_nothing():
break # Channel closed
process(msg.unwrap())
Work Queue¶
# Multiple workers can share cloned senders
let ch = channel_new(100)
let sender1 = ch.sender()
let sender2 = sender1.clone() # Multiple producers
# Single consumer processes all work
let receiver = ch.receiver()
When to Use Channels¶
Use channels when: - Tasks need to communicate results - You want to decouple producers from consumers - Order of messages matters (FIFO)
Consider alternatives: - Mutex: For shared state that multiple tasks read/write - TaskGroup: For structured concurrency (waiting for subtasks)
Comparison: Channel vs Mutex¶
| Feature | Channel | Mutex |
|---|---|---|
| Pattern | Message passing | Shared state |
| Blocking | Yes (when full/empty) | Yes (when locked) |
| Data flow | One-way | In-place |
| Best for | Pipelines, queues | Counters, caches |
See Also¶
- Mutex - Thread-safe shared state
- Concurrency Overview - Thread safety and tasks