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:
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.
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. This enables cleaner code by processing
jobs using ch.as_list.map action as follows:
A few minor cleanups further improve this example: Waiting for thread
termination using join allows closing the result channel, which
enables using res.as_list.sum to get the total sum: