Sterling has too many projects Blogging about Raku programming, microcontrollers & electronics, 3D printing, and whatever else...

Channels

| 520 words | 3 minutes | raku advent-2019
Venice canal

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.

Channels 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 Channels are ideally suited for.

Cheers.

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).

Image credit: unsplash-logoTania Miron