Go’s Concurrency and Channel Internals
Table of Contents
Go is implementing CSP (Communicating Sequential Processing): processes are communicating through channels, they can block each other while waiting for read/writes to channels.
Actor model makes inter-process communications more explicit and non-blocking.
CSP vs Actor explained — https://dev.to/karanpratapsingh/csp-vs-actor-model-for-concurrency-1cpg.
Channels requirements
- goroutine-safe
- store and pass data across goroutines
- FIFO
- can block/unblock goroutines
Internals
Channels are created in the heap as they have to be safely shared across goroutines.
It lives in runtime/chan.go
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // ring/circular queue. points to an array of dataqsiz elements elemsize uint16 closed uint32 // 0/1 is closed or not, atomic friendly elemtype *_type // element type sendx uint // send index in buf recvx uint // receive index in buf recvq waitq // list of recv waiters, sudog linked list sendq waitq // list of send waiters, sudog linked list lock mutex } |
Important fields are shown below:
- buf is a link to a ring queue of stored elements in the channel’s buffer
- sendx, recvx are indices in that buffer to store/get a value (in FIFO manner)
- sendq, recvq are the linked lists of sudog of goroutines and values waiting for sending/receiving values
- lock is a mutex guarding all operations related to the channel data manipulations
Sender goroutine will copy data to position sendx (unless qcount is reached) and increment sendx (or set it to 0).
Receiver goroutine will copy data from position recvx (unless qcount is 0) and increment recvx (or set it to 0).
If there are no values in buf, receiver is paused. If buf is full, sender is paused.
Pausing/resuming goroutines
Sender blocking
When a goroutine G1 tries to send, but the buffer is full, channel (chansend() function) does the context switch: it calls gopark()
runtime function to set G1’s state to «waiting». Scheduler unlinks G1 from OS thread (more on scheduler — https://blog.bullgare.com/2022/11/go-scheduler-details/) and G1 waits until someone is reading from the channel.
G1 is stored in sendq
together with the value it wants to send.
Eventually, another goroutine, G2, decides to receive from the channel. It reads from the buffer, increases sendx index, and then the magic happens. chanrecv() function checks the sendq
and sees the sudog with G1 there. G2 copies elem
from the sudog to the buf
and calls runtime’s goready()
to set G1’s state to runnable.
Receiver blocking
When a goroutine G3 is willing to receive from the channel, and there is no data yet, chanrecv() function pauses G3 and adds it to recvq
together with the pointer to memory where it expects the data to be copied to. Then G3 is gopark
ed as we discussed before.
When G4 decides to send to the channel, chansend() function first check if recvq
is not empty, notices the sudog with G3 and elem pointer in it, and copies the value DIRECTLY to G3’s elem or G3’s stack. Then it also sets G3’s state to runnable.
Unbuffered channels
They do not use a buffer at all, and sender is always copies directly to the receiver’s stack, which is faster as it does not involve extra locks.
Select
With default
1 2 3 4 5 6 7 8 |
select { case a := <-chA: fmt.Println("received from A:", a) case chB <- "b": fmt.Println("sent to B") default: fmt.Println("nothing found") } |
It’s non-blocking, which means that this goroutine checks in random order if there is any value in chA
‘s buffer, or if chB
‘s buffer is not full. If any of those is true, it will run that case. Otherwise it will run the default
case and continue the execution.
Without default
1 2 3 4 5 6 |
select { case a := <-chA: fmt.Println("received from A:", a) case chB <- "b": fmt.Println("sent to B") } |
It’s blocking. It first checks if chA or chB are ready to send/receive. If they are, it executes the case.
But if they are not ready, it will add current goroutine in chA
‘s receiver queue and chB
‘s sender queue and will wait until any of them is ready. Then it is removed from all other queues and continues the execution.
Resources
https://www.youtube.com/watch?v=KBZlN0izeiY
More detailed talk in Russian
https://www.youtube.com/watch?v=ZTJcaP4G4JM
Similar Posts
LEAVE A COMMENT
Для отправки комментария вам необходимо авторизоваться.