Exploring Plan9 libthread

The other day i was trying to solve a wonderful advent of code puzzle while at
the same time trying to learn more about programming in Plan9.  The puzzle in
question was 2022, day5.  where supposedly you control a giant crane adding and
removing boxes from different piles.  You start with an initial configuration of
boxes in different piles and then you can pick up one and place it on another.
In the end you want the state of each stack.  There are probably billions of
more efficient approaches to this problem but it originally got me thinking on
how i could use the function call stack.  The observation of having to maintain
state in different stacks somehow got me thinking about threads, and i decided
to take a look at the wonderful libthread of plan9.  From the
manual page thread(2)
we see that the programming model involves both channels (event-based) and
shared memory mechanisms.  It also is quick to point out the ability to spawn
processes (procs) that can contain threads, or just threads within a proc.  We
also learn that threads contained within a proc share memory so it is valid to
pass pointers from one to the other.  Having written a fair share of golang i
was directly intrigued from the Alt/alt() construct.  This is in essence a
select on multiple channels.  It can accept various channels for the thread to
send or receive data, and if some actions are readily doable it will pick a
random one and do it.  Otherwise it will block until one of the actions in the Alt
struct are doable.  Constructs to golang exist to notify downstream processing
threads of exiting by closing th they listen on.  If you close all the channels
that a thread is doing an alt() on, it return -1 and exit.

In terms of resources to learn more about libthread, the manual page is pointing
us to
/sys/src/libthread/example.c
a sample program that will read mouse click events and clock events.  One other
thing to notice here is that this program is firing up more processes, to
actually read the mouse events.  This is because blocking system calls like
read() will block the whole proc (and all the threads that are executing inside
it).  Therefore it seems that when dealing with I/O (or generally blocking
calls) we should do so in different dedicated procs and let the operating system
deal with their scheduling.  Another sample program exists in the same
directory, which is a very elegant concurrent prime number sieve
/sys/src/libthread/tprimes.c
Finally there is also a fantastic talk by rsc at
his homepage
which really goes into great detail of the implementation of libthread and the
design choices.

Since i have found the pattern of one worker thread needing to communicate with
multiple workers over pairs (or triplets if we want to have control messages) of
channels quite a common one in golang, i wrote
this demo program.
The main thread will spawn N (5 in this case) threads and write one char at
each.  It will then read from those threads in reverse.  The worker threads are
utilizing the alt construct while the main thread explicitly send() and recv().

Looking at libthread really made me appreciate both Plan9 and its libthread as
well as golang more.  The design choices and the amount of polishing, to make
golang IPC usable by people just want powerful concurrent primitives, have their
roots in libthread.  On the other hand when writing golang, when questions
involving task affinity, cache locality etc of the data being exchanged over
channels arise, the answers are usually quite complex and sometimes we discover
that the runtime won't allow certain operations (and for good reasons).  However
seeing how libthread on Plan9 was intended to work, straight from the manual
page we get powerful primitives to deal with concurrency both in thread and in
process, as well as clarity and control on how the operating system scheduler is
going to interact with us.
- - - - - - - - -
This content is also hosted over gemini