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.