tags | ||
---|---|---|
|
At a high level a channel is a pointer to the struct with a buffer, two wait queues and a lock
A channel is a pointer to a hchan
struct allocated on the heap:
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dexataqsiz elements
elemsize uint16
closed uint32
timer *timer // timer feeding this chan
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
// waitq represents stores a taild and a head of the waiters list
type waitq struct {
first *sudog
last *sudog
}
The main fields are:
buf
a circular FIFO queue storing elements of the channel (sendx
andrecvx
are indexes to head and tail)recvq
a linked list of goroutines represented bysudog
struct waiting for receiving data from the channelsendq
a linked list of goroutines represented bysudog
struct waiting to send data on the channellock
a runtime version of a mutex protecting fields inhchan
andsudog
Enqueueing an element to the buf
causes a copy of the value to be added to the buf
. Dequeuing an element from the buf
causes a copy of the buf
element to be assigned to the variable. It provides channels memory safety guarantees, as the only shared memory is a hchan
protected by a lock
Represents a goroutine (pseudo-goroutine) stored in a wait list:
// sudog (pseudo-g) represents a g in a wait list, such as for sending/receiving
// on a channel.
//
// sudog is necessary because the g ↔ synchronization object relation
// is many-to-many. A g can be on many wait lists, so there may be
// many sudogs for one g; and many gs may be waiting on the same
// synchronization object, so there may be many sudogs for one object.
//
// sudogs are allocated from a special pool. Use acquireSudog and
// releaseSudog to allocate and free them.
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
// The following fields are never accessed concurrently.
// For channels, waitlink is only accessed by g.
// For semaphores, all fields (including the ones above)
// are only accessed when holding a semaRoot lock.
acquiretime int64
releasetime int64
ticket uint32
// isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
// success indicates whether communication over channel c
// succeeded. It is true if the goroutine was awoken because a
// value was delivered over channel c, and false if awoken
// because c was closed.
success bool
// waiters is a count of semaRoot waiting list other than head of list,
// clamped to a uint16 to fit in unused space.
// Only meaningful at the head of the list.
// (If we wanted to be overly clever, we could store a high 16 bits
// in the second entry in the list.)
waiters uint16
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
The main fields are:
g
a pointer to the waiting goroutineelem
an elementg
is waiting to send/receivenext
/prev
pointers to the next/previousg
in a wait listhchan.recvq
/hchan.sendq
Actions when G
sends on a non-full and non-empty channel:
- Acquire the
lock
- Enqueue the element to the
buf
. It stores a copy of the element in thebuf
- Release the
lock
Actions when G
sends on a full channel (blocking call):
- Acquire the
lock
- Create a
sudog
for ourselves with element we are waiting to send and enqueue it to thesendq
. A receiver will use it to resume us in the future - Make a
gopark
call to the scheduler and release thelock
- Scheduler changes
G
state from running to waiting and removes association betweenG
andM
- Scheduler dequeues the runnable
G'
from the run queue and schedules it ontoM
Essentially a user-level context switch, we blocked the G
but not the OS thread M
that started running a G'
Actions when G
sends on an empty channel and there is a blocked receiver G'
:
- Acquire the
lock
- Dequeue the
sudog
struct of the waiting receiverG'
from therecvq
- Directly copy the element to the
element
field of the waiting receiverG'
sudog
. An optimization, when the blocked goroutineG'
resumes it doesn't have to acquire the lock and dequeue the element frombuf
- Make a
goready(G')
call to the scheduler and release thelock
- Scheduler changes
G'
state from waiting to runnable and enqueues it to the run queue. It will be eventually scheduled since it's on the run queue G
continues executing
Actions when G
receives from a non-empty and non-full channel:
- Acquire the
lock
- Dequeue the element from the
buf
. It copies the element inbuf
to variable in assignment - Release the
lock
Actions when G
receives from an empty channel (blocking call):
- Acquire the
lock
- Create a
sudog
for ourselves with address we are waiting to receive to and enqueue it to therecvq
. A sender will use it to resume us in the future - Make a
gopark
call to the scheduler and release thelock
- Scheduler changes
G
state from running to waiting and removes association betweenG
andM
- Scheduler dequeues the runnable
G'
from the run queue and schedules it ontoM
Actions when G
receives from a full channel and there is a blocked sender G'
:
- Acquire the
lock
- Dequeue the element from the
buf
- Dequeue the
sudog
struct of the waiting senderG'
from thesendq
- Enqueue the element
elem
from thesudog
into thebuf
. An optimization, when the blocked goroutineG'
resumes it doesn't have to acquire the lock to enqueue the element - Make a
goready(G')
call to the scheduler and release thelock
- Scheduler changes
G'
state from waiting to runnable and enqueues it to the run queue. It will be eventually scheduled since it's on the run queue G
continues executing
Unbuffered channels always work as direct send case
- Receiver waiting → sender directly writes to receiver's stack from
sudog
- Sender waiting → receiver directly writes to sender's stack from
sudog
- All channels are locked
- A
sudog
is put in thesendq
/recvq
queues of all channels - Channels unlocked, all the selecting G is paused
- CAS operation so there is only one winning case
- Resuming mirrors the pause sequence