Channel
Atomics and locks let threads share data safely. A channel lets them share nothing at all: one thread sends a value, and the value moves to whichever thread receives it. Each value has one owner at a time, so there is nothing to guard. Many concurrent programs are easiest to write as a line of stages, each on its own thread, passing work down the line.
import Sync::Channel;
A bounded queue
A Channel<T> is a queue with room for a fixed number of values, its capacity, set when it is made. Values come out in the order they went in.
| Method | Does | Returns |
|---|---|---|
Send(v) | adds v, waiting while the channel is full | ! T: fails if closed |
TrySend(v) | adds v if there is room, without waiting | ! T: fails if full |
Receive() | takes the oldest value, waiting while the channel is empty | T?: none once drained |
TryReceive() | takes the oldest value if there is one, without waiting | T? |
Close() | ends the stream: no more sends | nothing |
Because Send waits while the channel is full, a fast producer cannot run ahead of a slow consumer and pile up values without limit: it is slowed to the consumer's pace. That is called backpressure. The channels here have room for just two values each.
Like Mutex<T>, a channel is a handle, and Clone gives each thread one of its own.
A pipeline
The program is three stages connected by two channels. Generate sends the numbers 1 to 10, Square squares each one, and Main adds them up:
flowchart LR
g["`Generate`"] -- "numbers" --> s["`Square`"]
s -- "squares" --> m["`Main`"]let numbers = Channel<uint64>(2);
let squares = Channel<uint64>(2);
var producer = Spawn(Generate, numbers.Clone()) catch { else => return 1 };
var squarer = Spawn(Square, Stage { input: numbers.Clone(), output: squares.Clone() }) catch {
else => return 1
};
The middle stage holds one end of each channel. It receives until its input runs dry, sends each square on, and then closes its own output:
func Square(stage: Stage) -> () {
while true {
match stage.input.Receive() {
value? => stage.output.Send(value * value) catch { else => return },
none => break
}
}
stage.output.Close();
}
Receive returns uint64?. A value means work to do; none means the channel is closed and empty, so nothing more will ever come. Only Main prints, and with one sender per channel the squares arrive in exactly the order they were sent.
Closing
A receiver cannot tell "nothing yet" from "nothing ever" on its own. The sender says so by calling Close when it is finished, and each stage passes the end of the stream down the line by closing its own output once its input is done.
Closing does not throw away what was already sent: receivers still take every value left in the queue, and only then get none.
let late = Channel<uint64>(4);
late.Send(7) catch { else => return 1 };
late.Send(8) catch { else => return 1 };
late.Close();
After the Close, late still gives 7, then 8, then none. A Send to a closed channel fails — and hands the value back as its failure, so nothing is lost silently:
match late.Send(9) {
.Success(_) => PrintLine("sent 9"),
.Failure(value) => PrintLine("closed, {} came back", value)
}
Any number of threads may send to one channel and receive from it. With several senders the values of each one still arrive in order, but how the senders' values interleave depends on the scheduling. Close the channel only once every sender has finished — for instance after joining them all.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Locks let threads share data. A channel lets them share nothing: one thread sends a value, and
// the value moves to whichever thread receives it. Each value has one owner at a time, so there
// is nothing to guard.
//
// A `Channel<T>` is a queue with room for a fixed number of values. `Send` waits while it is full
// and `Receive` waits while it is empty, so a fast producer is slowed to the pace of its consumer
// instead of piling up values without limit. Values arrive in the order they were sent.
//
// When the producer has nothing more to send, it calls `Close`. Receivers still take what is left
// in the queue, and then `Receive` returns none: that is how a consumer knows the stream is over.
// Like `Mutex<T>`, a channel is a handle, and `Clone` gives each thread its own.
import Io::{ Print, PrintLine };
import Sync::Channel;
import Thread::{ JoinHandle, Spawn };
// The first stage sends 1 to 10, then closes its channel.
func Generate(output: Channel<uint64>) -> () {
for value in 1u64..=10u64 {
// `Send` fails, handing the value back, only once the channel is closed.
output.Send(value) catch { else => return };
}
output.Close();
}
// The middle stage's two ends.
struct Stage {
input: Channel<uint64>;
output: Channel<uint64>;
}
// The second stage squares each value it receives until its input runs dry, then closes its own
// output, so the end of the stream passes down the line.
func Square(stage: Stage) -> () {
while true {
match stage.input.Receive() {
value? => stage.output.Send(value * value) catch { else => return },
none => break
}
}
stage.output.Close();
}
func Main() -> int {
// Two small channels: neither stage can run more than two values ahead of the next.
let numbers = Channel<uint64>(2);
let squares = Channel<uint64>(2);
var producer = Spawn(Generate, numbers.Clone()) catch { else => return 1 };
var squarer = Spawn(Square, Stage { input: numbers.Clone(), output: squares.Clone() }) catch {
else => return 1
};
// Only `Main` prints. It receives the squares in order, and stops when the stream ends.
// `Receive` returns `uint64?`, so `let value: uint64 = squares.Receive();` is refused:
//
// error: cannot assign 'uint64?' to 'uint64'
var total: uint64 = 0;
Print("squares:");
while true {
match squares.Receive() {
value? => {
Print(" {}", value);
total = total + value;
},
none => break
}
}
PrintLine("");
PrintLine("total: {}", total);
producer.Join();
squarer.Join();
// Closing does not throw away what was already sent: the queue drains first.
let late = Channel<uint64>(4);
late.Send(7) catch { else => return 1 };
late.Send(8) catch { else => return 1 };
late.Close();
let first = late.Receive() ?? 0;
let second = late.Receive() ?? 0;
let third = late.Receive();
PrintLine("after close: {}, {}, then {}", first, second, third == none ? "none" : "more");
// A send to a closed channel fails and returns the value, so nothing is lost silently.
match late.Send(9) {
.Success(_) => PrintLine("sent 9"),
.Failure(value) => PrintLine("closed, {} came back", value)
}
return 0;
}
Besides Io, its Rux.toml lists Sync and Thread under [Dependencies].
Run it
cd Examples/Concurrency/Channel
rux run
squares: 1 4 9 16 25 36 49 64 81 100
total: 385
after close: 7, 8, then none
closed, 9 came back
Common mistakes
Try it yourself
- Add a third stage between
SquareandMainthat passes on only the even squares. - Start two
Squarethreads on the same pair of channels. Each closessquareswhen it is done — why can that lose values, and how wouldMainclose it instead? - Make
GenerateuseTrySend, and count how often the channel was full.
Learn more
- Sync — the package in the API reference
- Presence — matching
value?andnone - Mutex — sharing data instead of passing it
- Wait group — the next lesson: waiting for a whole group of threads
25.4 Arc
Share one value between threads with Arc<T>, a counted handle whose copies may be made and dropped on any thread, and change what must change inside it through an Atomic<T>.
25.6 Wait group
Wait for a group of detached workers with a WaitGroup — Add before they start, Done as each one's last step, Wait in Main — and collect their answers in numbered slots so the output keeps its order.