Channels
A Channel
, in Raku, is an asynchronous
queue of data. You can feed data in to one end of the queue and receive data at
the other end safely, even when multiple threads are involved.
Let’s consider a variant of the Dining Philosopher Problem: We have five philosophers eating soup at the table. They do not talk to one another because they are too busy thinking about philosophy. However, there are only 2 spoons. Each time a philosopher wishes to take a sip of soup, she needs to acquire a spoon. Fortunately, each philosopher is willing to share spoons and sets her spoon at the center of the table after finishing each bite.
We can model this problem like this:
my $table = Channel.new;
my @philosophers = (^5).map: -> $p {
start {
my $sips-left = 100;
while $sips-left > 0 {
my $spoon = $table.receive;
say "Philosopher $p takes a sip with the $spoon.";
$sips-left--;
sleep rand;
$table.send($spoon);
sleep rand;
}
}
}
$table.send: 'wooden spoon';
$table.send: 'bamboo spoon';
await Promise.allof(@philosophers);
Here we have five tasks running in five threads, each contending for one of two resources. They will each take 100 sips of soup. Running this program will give you 500 lines of output similar to this:
...
Philosopher 0 takes a sip with the wooden spoon.
Philosopher 2 takes a sip with the bamboo spoon.
Philosopher 3 takes a sip with the wooden spoon.
Philosopher 1 takes a sip with the bamboo spoon.
Philosopher 4 takes a sip with the bamboo spoon.
Philosopher 2 takes a sip with the bamboo spoon.
Philosopher 0 takes a sip with the wooden spoon.
Philosopher 1 takes a sip with the wooden spoon.
Philosopher 0 takes a sip with the wooden spoon.
...
The code itself is very simple. We start
five threads each representing a
philosopher. Each philosopher calls to .receive
the next available spoon. This
method will block until a spoon becomes available. The philosopher takes a sip
and then uses .send
to return the spoon to the table for anyone else to use.
Eventually, the philosopher finishes 100 sips and the Promise
returned by
start
will be kept.
The main thread kicks off the process by placing two spoons on the table using
.send
. Then, it uses await
to keep the program running until all the
philosopher tasks are complete.
Channel
s have very low overhead as far as CPU is concerned. The senders will not
wait on the receivers. The receivers can either block until something is
available in the queue using .receive
or they can use .poll
to check for
items without blocking. The cost is transferred to memory. The Channel
must
store all sent items internally until they are received and the queue will
continue to grow until the program runs out of memory.
Therefore, a Channel
is helpful when you have resources or messages you want
to distribute, but not share between tasks. Or when you just need to communicate
point to point. Job queues, resource pools, and peer-to-peer task communication
are good examples of the sort of problems Channel
s are ideally suited for.
Cheers.