In the previous post, we discussed the basics of Go channels and how they facilitate communication between goroutines. In this post, we will delve deeper into the internal workings of Go channels, specifically focusing on the concept of “sudog” and how it plays a crucial role in channel operations.
Let’s recap the hchan structure that represents a channel in Go’s runtime:
type hchan struct {
qcount uint // number of elements in queue
dataqsiz uint // buffer capacity
buf unsafe.Pointer // circular buffer
elemsize uint16 // size of each element
closed uint32 // is channel closed?
elemtype *_type // element type (for reflection)
sendx uint // send index (next slot to write)
recvx uint // recv index (next slot to read)
recvq waitq // list of waiting receivers
sendq waitq // list of waiting senders
lock mutex // protects all fields
}
sudog
A sudog (sudo goroutine) is a runtime data structure in Go that represents a goroutine’s waiting state on a synchronization primitive.
sudog in channel
So how is the waitq implemented? It is a linked list of sudog structures, The sudog structure represents a
goroutine that is waiting to send or receive on a channel.
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g // goroutine
next *sudog // next sudog
prev *sudog // previous sudog
elem unsafe.Pointer // pointer to element
c *hchan // channel
// ... other fields ...
}
So for the channel, we may see the sendq and recvq as below:
sendq
┌─────────┐ ┌─────────┐ ┌─────────┐
│ sudog1 │───→│ sudog2 │───→│ sudog3 │
│ g: G1 │ │ g: G2 │ │ g: G3 │
│ elem: * │ │ elem: * │ │ elem: * │
└─────────┘ └─────────┘ └─────────┘
recvq
┌─────────┐ ┌─────────┐
│ sudog4 │───→│ sudog5 │
│ g: G4 │ │ g: G5 │
│ elem: * │ │ elem: * │
└─────────┘ └─────────┘
sudog in other places
Actually, sudog is not only used in channels, but also in other places like sync.Cond, WaitGroup, Mutex and so
on.
So we can describe sudog as a general structure to represent a goroutine that is waiting for some event to happen.
sudog = {
goroutine: waiting goroutine,
sync_object: the object the goroutine is waiting on (channel, mutex, cond, etc.),
operation: send/receive/lock/wait,
context: additional context (like the element to send/receive),
}
The relationship between goroutine and sudog
One goroutine can have multiple sudogs, but one sudog only belongs to one goroutine.
// In a select statement
select {
case <-ch1: // sudog1: same goroutine waiting on ch1
case <-ch2: // sudog2: same goroutine waiting on ch2
case <-ch3: // sudog3: same goroutine waiting on ch3
}
--->
sudog1 ────→ channel1.recvq
┌─────────────┐
│ sudog1 │
│ c: ─────────┼───→ channel1
│ elem: ──────┼───→ &v1
│ g: ─────────┼───→ current_goroutine
└─────────────┘
sudog2 ────→ channel2.recvq
┌─────────────┐
│ sudog2 │
│ c: ─────────┼───→ channel2
│ elem: ──────┼───→ &v2
│ g: ─────────┼───→ current_goroutine (same one)
└─────────────┘
sudog3 ────→ channel3.sendq
┌─────────────┐
│ sudog3 │
│ c: ─────────┼───→ channel3
│ elem: ──────┼───→ &val
│ g: ─────────┼───→ current_goroutine (same one)
└─────────────┘
Here, one goroutine is waiting on multiple channels, so it has multiple sudogs.