Communicating via Channels
Channels provide a means to send data —input to be processed, results, etc.— between threads. A channel can send data from one or several sending threads to one or several receiving threads. Each item that was sent will be received by exactly one receiving thread.
Sending and receiving may cause a thread to block: a receiving thread might have to wait for the availability of data (or closing of the channel), while a sending thread might have to wait in case the channel's capacity to store items is exhausted.
A single-threaded example
Let's illustrate channels with a simple task: For a given interval of
integers, we would like to count the numbers with a certain property. Here, we
just use is symmetric as our property, i.e., numbers
like 171, 4 or 4224 are symmetric,
while 13 or 991 are not. We can check this with the
following code:
Splitting up the Work
Before we distribute this task, we need to find a way to split it into
smaller jobs that can then be run in parallel. We can split up the interval
into njobs smaller intervals and count each individually as
follows, still using only a single thread:
Running jobs in parallel, collecting results via a channel
Now that we have defined the jobs, we could start one thread per job and
execute them in parallel. The tricky part, however, is is to collect the
results. We use a channel res for this and have every thread
push its result to that channel using res <- r. In the main
thread, we collect all the results and sum them up:
Note that a channel internally has mutable state and requires a mutate
effect. The base library provides effect channel_mutate for this
purpose. If you perform effects analysis of the code by clicking
on Effects! for this example, you will see the
effect channel_mutate i32 showing you that this code requires
inter-thread channels of i32 values.
Distributing jobs using a channel
This example runs exactly one job per thread. This is usually not ideal since
we will have to wait for the thread whose job processing takes longest, while
other threads might be idling after they are done with their job. The
following example uses more jobs than threads and a second
channel ch to distribute the jobs to the threads.
Note that we now need a means to inform a thread that the end of the sequence
of jobs is reached. This is done by closing the channels
using ch.close, after which <-ch will
return the empty option.
When doing the effects analysis of this code (by clicking on Effects!
for this example), you will see that now there are two
effects channel_mutate (interval i32) for the jobs
and channel_mutate i32 for the results. For more clarity in a
larger system, the data sent through a channel could be wrapped in a feature
with a meaningful name that will then show up in the effects analysis.
Channels as lazy lists
Using a loop to process the jobs is a little clumsy. A channel provides an
alternative: the values can be provided as a lazy list
using ch.as_list or res.as_list. This enables
cleaner code by processing jobs using,
e.g., ch.as_list.map action as follows: