This is a creation in Article, where the information may have evolved or changed.
This article attempts to explain the implementation of channel and select in Go runtime, some of which are from gophercon2017. Go version is 1.8.3
Channel
The first section describes the use of the channel. The channel can be seen as a queue for communication between multiple goroutine, such as the following example, one goroutine sends the MSG, and the other MSG accepts the message. Channel is divided into buffer and without buffering, the difference is not very large, specifically please Google. See a simple example of how the channel is used.
package mainimport "fmt"func main() { // Create a new channel with `make(chan val-type)`. // Channels are typed by the values they convey. messages := make(chan string) // Send a value into a channel using the `channel <-` // syntax. Here we send `"ping"` to the `messages` // channel we made above, from a new goroutine. go func() { messages <- "ping" }() // The `<-channel` syntax receives a value from the // channel. Here we'll receive the `"ping"` message // we sent above and print it out. msg := <-messages fmt.Println(msg)}
function Points of channel:
- Queue
- Blocking
- When one end is blocked, it can be awakened by the other side
We start with this 3-point function and talk about concrete implementations.
Channel structure
Annotations are marked with several important variables, which can be broadly divided into two functional units, one ring buffer for storing data, and one for the goroutine queue.
type hchan struct { qcount uint // 当前队列中的元素个数 dataqsiz uint // 缓冲队列的固定大小 buf unsafe.Pointer // 缓冲数组 elemsize uint16 closed uint32 elemtype *_type // element type sendx uint // 下一次发送的 index recvx uint // 下一次接收的 index recvq waitq // 接受者队列 sendq waitq // 发送者队列 // 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}
Ring Buffer
The function is mainly composed of the following variables, a BUF store the actual data, two pointers for the send, receive the index position, with size, count in the array size range of sliding back and forth.
qcount uint // 当前队列中的元素个数dataqsiz uint // 缓冲队列的固定大小buf unsafe.Pointer // 缓冲数组sendx uint // 下一次发送的 indexrecvx uint // 下一次接收的 index
For example, suppose we initialize a channel with a buffer, the value of ch := make(chan int, 3) its initial state is:
qcount = 0dataqsiz = 3buf = [3]int{0, 0, 0} // 表示长度为3的数组sendx = 0recvx = 0
The first step is to send a value to the channel, ch <- 1 because the buffer is not yet full, so the post-operation status is as follows:
qcount = 1dataqsiz = 3buf = [3]int{1, 0, 0} // 表示长度为3的数组sendx = 1recvx = 0
Fast forward two, send two values (2, 3) to the channel continuously, with the following status:
qcount = 3dataqsiz = 3buf = [3]int{1, 2, 3} // 表示长度为3的数组sendx = 0 // 下一个发送的 index 回到了0recvx = 0
Receive a value from the channel <- ch with the following status:
qcount = 2dataqsiz = 3buf = [3]int{1, 2, 3} // 表示长度为3的数组sendx = 0 // 下一个发送的 index 回到了0recvx = 1 // 下一个接收的 index
Blocking
We look at how no data in the channel buffer is handled if the channel is receive. Logic in the chanrecv method, its approximate flow is as follows, preserving only the code of the blocking operation.
Func chanrecv (t *chantype, C *hchan, ep unsafe. Pointer, block bool) (selected, received bool) {//check if Channdel is nil//when not blocked, check buffer size, current size, check if Chennel is off, see See if you can return directly//check whether the sender has a waiting goroutine, the lower section mentions//current buffer has data, then try to remove. If non-blocking, direct return//No sender waits, no data in buffer, block wait. GP: = GETG () Mysg: = Acquiresudog () mysg.releasetime = 0 if T0! = 0 {mysg.releasetime =-1}//No Stack splits between assigning elem and enqueuing mysg//on gp.waiting where copystack can find it. Mysg.elem = EP Mysg.waitlink = Nil gp.waiting = Mysg MYSG.G = GP Mysg.selectdone = Nil mysg.c = C Gp.par am = Nil C.recvq.enqueue (MYSG)//Key operation: Set Goroutine status to waiting, separate G and M goparkunlock (&c.lock, "Chan Receiv E ", TRACEEVGOBLOCKRECV, 3)//Someone woke us up//wake up, clean sudog if mysg! = gp.waiting {throw (" G waiting List is corrupted ")} gp.waiting = nil If mysg.releasetime > 0 {blockevent (mysg.releaseTime-t0, 2)} closed: = Gp.param = Nil Gp.param = Nil mysg.c = nil Releasesudog (MYSG) return True,!clo Sed
The operation here is to create a current Goroutine Sudog, and then put this sudog into the channel of the recipient wait queue, set the current state of G, and M separate, to this point the current G is blocked, the code will not be executed.
When awakened, performs a sudog cleanup operation. The pointer to accept the value in buffer is ep this variable, and is awakened as if there is no ep action assigned to the value. We'll talk about this in the next section.
Sudog
One last question remains, when a goroutine is blocked by the channel, and the other goroutine how to wake it up.
There are two waitq types of variables in the channel, look at the structure of the discovery, is sudog linked list, the key is Sudog. Sudog contains a reference to Goroutine, note elem the variable, and the comment says it might point to the stack.
type waitq struct { first *sudog last *sudog}type sudog struct { // The following fields are protected by the hchan.lock of the // channel this sudog is blocking on. shrinkstack depends on // this. g *g selectdone *uint32 // CAS to 1 to win select race (may point to stack) next *sudog prev *sudog elem unsafe.Pointer // data element (may point to stack) // The following fields are never accessed concurrently. // waitlink is only accessed by g. acquiretime int64 releasetime int64 ticket uint32 waitlink *sudog // g.waiting list c *hchan // channel}
Speaking of blocking part of the time, we saw Goroutine was dispatched before, there is an enqueue operation, then, the current G Sudog has been deposited recvq , we look at the delivery of the operation.
The operation here is that the value sent by sender is copied directly to the Sudog.elem. Then wake up the SUDOG.G so that the receiver goroutine on the opposite side is awakened. Please specify the following comments.
Func chansend (t *chantype, C *hchan, ep unsafe. Pointer, block bool, CALLERPC uintptr) bool {//Check work//If you can eject Recvq from Chennel Sudog, then direct send if SG: = C.RECVQ . Dequeue (); SG! = Nil {//Found a waiting receiver. We pass the value we want to send//directly to the receiver, bypassing the channel buffer (if any). Send (c, SG, EP, func () {Unlock (&c.lock)}) return true}//buffer has free space, return; blocking Operation}func Send (c *hchan, SG *sudog, EP unsafe. Pointer, UNLOCKF func ()) {//Handle index//critical if sg.elem! = Nil {//Here is copy memory according to Elemtype.size Senddi Rect (C.elemtype, SG, EP) Sg.elem = nil}//some processing//reset Goroutine status, wake it Goready (GP, 4)}func Senddire CT (t *_type, SG *sudog, src unsafe. Pointer) {//SRC is on our stack, and DST is a slots on another stack. Once we read Sg.elem out of SG, it'll no longer//be updated if the destination ' s stack gets copied (shrunk). So make sure this no preemption points Can happen between read & use. DST: = Sg.elem typebitsbulkbarrier (t, UIntPtr (DST), UIntPtr (SRC), t.size) memmove (DST, SRC, t.size)}//memmove copie s n bytes from ' from ' to ' to './/In Memmove_*.s//go:noescapefunc memmove (to, from unsafe. Pointer, n uintptr)
Select
When looking at the chanrecv() method, a block parameter was found, which indicates whether the operation was blocked. In general, the channel is blocked (regardless of buffer), when is it not blocked?
The first thing to think about is select, when you write the default case, the other channel is non-blocking.
There is another may not commonly used, is the channel reflection value, can be non-blocking, this method is public, we first look at the simple.
func (v Value) TryRecv() (x Value, ok bool)func (v Value) TrySend(x Value) bool
Select is a little more complex, first found a note in the source code:
// compiler implements//// select {// case c <- v:// ... foo// default:// ... bar// }//// as//// if selectnbsend(c, v) {// ... foo// } else {// ... bar// }//func selectnbsend(t *chantype, c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(t, c, elem, false, getcallerpc(unsafe.Pointer(&t)))}// compiler implements//// select {// case v = <-c:// ... foo// default:// ... bar// }//// as//// if selectnbrecv(&v, c) {// ... foo// } else {// ... bar// }//func selectnbrecv(t *chantype, elem unsafe.Pointer, c *hchan) (selected bool) { selected, _ = chanrecv(t, c, elem, false) return}
If it is a case + default mode, then the compiler calls the above method to implement.
What if it's multiple case + default mode? How does the select run on runtime exactly? Write a simple select to compile a bit.
package mainfunc main() { var ch chan int select { case <-ch: case ch <- 1: default: }}
go tool compile -S -l -N test.go > test.sFind keywords in the results, for example:
0x008c 00140 (test.go:5) CALL runtime.newselect(SB)0x00ad 00173 (test.go:6) CALL runtime.selectrecv(SB)0x00ec 00236 (test.go:7) CALL runtime.selectsend(SB)0x0107 00263 (test.go:8) CALL runtime.selectdefault(SB)0x0122 00290 (test.go:5) CALL runtime.selectgo(SB)
Here selectgo is the actual way to run, look for, note the notes. First check whether the channel can operate, if not operation, go to the default logic.
Loop://Pass 1-look for something already waiting var DFL *scase var cas *scase for I: = 0; i < int (sel.ncase); i++ {cas = &scases[pollorder[i]] c = cas.c switch Cas.kind {//Accept data Case CASERECV : SG = c.sendq.dequeue ()//If sender is waiting for if sg! = nil {goto recv }//Current buffer has data if C.qcount > 0 {goto BUFRECV}//Off Closed Channel if c.closed! = 0 {goto Rclose} case casesend:if Raceen abled {racereadpc (unsafe. Pointer (c), cas.pc, CHANSENDPC)}//close if c.closed! = 0 {goto Sclose }//receiver is waiting for SG = c.recvq.dequeue () if SG! = nil {goto SE ND}//have space to accept if C.qcount < C.dataqsiz {gotoBufsend}//Go to default case CASEDEFAULT:DFL = cas}} if DFL! = Nil { Selunlock (scases, lockorder) cas = DFL goto retc}