NOTE: as per #79, we are first pursuing a single-producer/single-consumer (SPSC) channel implementation, which we will extend to a MPMC channel in a later effort. Therefore, this note assumes exactly one sender and one concurrent receiver.
sync.Cond
?Reference #1 says it best:
If I had to summarize sync.Cond in one sentence: A channel says “here’s a specific event that occurred;” a sync.Cond says “something happened, idk, you go figure it out.” Thus, sync.Cond is more vague, which means it’s also more flexible.
It also hints at how you should use it:
It’s kind of like a
chan struct{}
paired with async.Mutex
: after you receive from the channel, you lock the mutex, then go investigate what happened.
There are two parts to this:
sync.Cond
in conjunction with a mutex.sync.Cond
locks the mutex when it receives a signal, and then hands control to you.(Note: point #2 is a bit unclear from the phrasing, but that is how it works)
At any rate, ref #1 is worth reading to understand sync.Cond
in general.
If we trace our way through the implementation flowchart for #79 (below), it becomes clear that there are multiple points at which a goroutine must block until something has happened.
Exactly what must happen is, however, dependent on the current state of the flowchart. For example, "is it my turn?" requires checking different state variables than "is the receiver ready?". Moreover, because both the sender and receiver goroutine can modify these state variables, some synchronization is necessary (e.g. a mutex). Thus, at a glance, we have a problem well suited for sync.Cond
.
The following is a proof-of-concept implementation of the SPSC channel using sync.Cond
. To avoid obscuring the high-level mechanics, it includes some simplifying assumptions:
Applying this logic to a capnp capability server should be a fairly straightforward matter of:
call.Go()
to ensure that senders & receivers do not block each other.Implementation: https://go.dev/play/p/20Nht77ZgQ_9
The concurrency here is already non-trivial, and that complexity is bound to increase with the addition of MPMC semantics. The more I stare at this, the more I think this effort could benefit from a formalized approach. This could take several forms.
At a minimum, we should take the time to enumerate the channel invariants, and convince ourselves – even if informally – that they hold. I see roughly two (non-exclusive) approaches we could take:
There are some marginal behaviors of sycn.Cond
that I don't fully understand. While I would expect the Go devs to avoid (or at least document) any footguns, I would like to actually understand sync.Cond
's invariants in some detail. For instance: what happens when a thread calls Signal()
immediately before Wait()
, as in the above Send()
implementation? Is Send()
re-entrant in these conditions? If so, this would produce a livelock.
A cursory glance at the sync.Cond
implementation suggests that Signal()
< Wait()
does not produce re-entrant behavior. The call to Singal()
notifies threads that have previously registered in a runtime.notifyList
. So that's nice