Channel-based concurrency introduces questions about fairness, ordering, and delivery guarantees-especially in systems with multiple producers, consumers, or select() blocks competing over shared channels.
In pychanio, we follow Go-inspired semantics where possible, but also document any deviations explicitly. This chapter discusses what guarantees are provided today, and which are deferred to future versions.
1. Message Delivery Guarantees
β Guarantee: Messages Sent Before Close Are Delivered
If a channel is not closed and a producer successfully sends a message (await ch.send(x) or ch << x), that message will be eventually delivered to a receiver, unless the consumer exits early.
ch =chan()go(lambda: ch <<"msg")val, ok =await (ch >>None)# val == "msg", ok == True
β Guarantee: Closure Prevents Further Sends
If a channel is closed via close(ch), any subsequent send attempt raises ChannelClosed. Receivers may still read buffered items (if any), but no new data may be written.
close(ch)ch <<"oops"# Raises ChannelClosed
2. FIFO Ordering
Each pychanio channel uses an internal asyncio.Queue, which guarantees FIFO (first-in-first-out) ordering:
This guarantee applies per channel, but not across channels.
3. Select Fairness
β No Strong Fairness Across Select Cases
When multiple channel operations are passed to select(...), pychaniorandomly chooses one of the ready cases. This means:
You cannot assume round-robin scheduling
Some branches may be selected more frequently than others
Starvation is possible in certain configurations
Example
Even if both channels are ready, the selection is random.
Design Rationale
This behavior is intentional and mirrors Goβs design:
Select statements choose randomly among equally ready channels to prevent deterministic, fragile patterns.
If fairness is required, it must be enforced by user code (e.g., shuffle cases manually or alternate across calls).
4. Unbuffered Channels β Blocking (Yet)
Unlike Go, unbuffered channels in pychanio do not currently block the sender until a receiver is ready.
Instead, chan() (with capacity = 0) behaves as if it has an infinite buffer.
Characteristics (Current Behavior):
Sends complete immediately, regardless of receiver readiness
Receivers can read at any later point
No backpressure or flow control
Behaves like an unbounded queue internally
This is a known limitation and will be addressed in a future version. For now, do not rely on unbuffered channels for tight coordination.
See Chapter 4 for detailed workarounds and explanation.
5. Buffered Channels and Blocking
Buffered channels (e.g., chan(2)) do block the sender once the buffer is full. This enables:
Flow control between fast producers and slow consumers
Controlled throughput in pipelines
Receivers always block if the channel is empty.
6. Starvation and Manual Fairness
If fairness across channels matters to your application, consider: