The essence of GO language interview - what is the process of receiving data from channel?

Source code analysis

Let's first take a look at the source code related to receiving. After clarifying the specific process of reception, we will study it in detail based on a practical example.

There are two ways to write the receive operation. One is with "ok", which reflects whether the channel is closed; the other is without "ok". In this way, when a zero value of the corresponding type is received, it is impossible to know that it was sent by the real sender. value, or a zero value of the default type returned to the receiver after the channel is closed. Both writing methods have their own application scenarios.

After processing by the compiler, these two writing methods finally correspond to these two functions in the source code:

// entry points for <- c from compiled code
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
	_, received = chanrecv(c, elem, true)
	return
}

chanrecv1If the function handles the situation without "ok", chanrecv2it will return the "received" field to reflect whether the channel is closed. The received value is special and will be "put" elemat the address pointed to by the parameter, which is very similar to the writing method in C/C++. If the received value is omitted from the code, elem here is nil.

Anyway, finally turned to chanrecvfunctions:

// 位于 src/runtime/chan.go

// chanrecv 函数接收 channel c 的元素并将其写入 ep 所指向的内存地址。
// 如果 ep 是 nil,说明忽略了接收值。
// 如果 block == false,即非阻塞型接收,在没有数据可接收的情况下,返回 (false, false)
// 否则,如果 c 处于关闭状态,将 ep 指向的地址清零,返回 (true, false)
// 否则,用返回值填充 ep 指向的内存地址。返回 (true, true)
// 如果 ep 非空,则应该指向堆或者函数调用者的栈

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 省略 debug 内容 …………

	// 如果是一个 nil 的 channel
	if c == nil {
		// 如果不阻塞,直接返回 (false, false)
		if !block {
			return
		}
		// 否则,接收一个 nil 的 channel,goroutine 挂起
		gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
		// 不会执行到这里
		throw("unreachable")
	}

	// 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回
	// 当我们观察到 channel 没准备好接收:
	// 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待
	// 2. 缓冲型,但 buf 里没有元素
	// 之后,又观察到 closed == 0,即 channel 未关闭。
	// 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的,
	// 因此在这种情况下可以直接宣布接收失败,返回 (false, false)
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {
		return
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	// 加锁
	lock(&c.lock)

	// channel 已关闭,并且循环数组 buf 里没有元素
	// 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况
	// 也就是说即使是关闭状态,但在缓冲型的 channel,
	// buf 里有元素的情况下还能接收到元素
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(unsafe.Pointer(c))
		}
		// 解锁
		unlock(&c.lock)
		if ep != nil {
			// 从一个已关闭的 channel 执行接收操作,且未忽略返回值
			// 那么接收的值将是一个该类型的零值
			// typedmemclr 根据类型清理相应地址的内存
			typedmemclr(c.elemtype, ep)
		}
		// 从一个已关闭的 channel 接收,selected 会返回true
		return true, false
	}

	// 等待发送队列里有 goroutine 存在,说明 buf 是满的
	// 这有可能是:
	// 1. 非缓冲型的 channel
	// 2. 缓冲型的 channel,但 buf 满了
	// 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine)
	// 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部
	if sg := c.sendq.dequeue(); sg != nil {
		// Found a waiting sender. If buffer is size 0, receive value
		// directly from sender. Otherwise, receive from head of queue
		// and add sender's value to the tail of the queue (both map to
		// the same buffer slot because the queue is full).
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	// 缓冲型,buf 里有元素,可以正常接收
	if c.qcount > 0 {
		// 直接从循环数组里找到要接收的元素
		qp := chanbuf(c, c.recvx)

		// …………

		// 代码里,没有忽略要接收的值,不是 "<- ch",而是 "val <- ch",ep 指向 val
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		// 清理掉循环数组里相应位置的值
		typedmemclr(c.elemtype, qp)
		// 接收游标向前移动
		c.recvx++
		// 接收游标归零
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		// buf 数组里的元素个数减 1
		c.qcount--
		// 解锁
		unlock(&c.lock)
		return true, true
	}

	if !block {
		// 非阻塞接收,解锁。selected 返回 false,因为没有接收到值
		unlock(&c.lock)
		return false, false
	}

	// 接下来就是要被阻塞的情况了
	// 构造一个 sudog
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}

	// 待接收数据的地址保存下来
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.selectdone = nil
	mysg.c = c
	gp.param = nil
	// 进入channel 的等待接收队列
	c.recvq.enqueue(mysg)
	// 将当前 goroutine 挂起
	goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)

	// 被唤醒了,接着从这里继续执行一些扫尾工作
	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, !closed
}

The above code is commented in more detail. You can look at the source code line by line. Let's take a closer look.

  • If channel is a null value (nil), it will be returned directly in non-blocking mode. In blocking mode, the gopark function will be called to suspend the goroutine, which will continue to block. Because when the channel is nil, the only way to avoid blocking is to close it. However, closing a nil channel will panic again, so there is no chance to be awakened. You can look at the closechan function in more detail.

  • Just like the sending function, next we perform an operation that quickly detects failure and returns in non-blocking mode without acquiring a lock. By the way, when we usually write code, we find some boundary conditions and return quickly to make the code logic clearer, because there will be fewer normal situations and it will be more focused, and people who read the code will be more focused. Look at the core code logic.

	// 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回 (false, false)
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {
		return
	}

When we observe that the channel is not ready to receive:

  1. Non-buffered, there is no goroutine waiting in the send queue.
  2. Buffered, but there are no elements in buf

Afterwards, it was observed that closed == 0, that is, the channel was not closed.

Because the channel cannot be opened repeatedly, the channel was not closed during the previous observation. Therefore, in this case, you can directly declare the reception failure and return quickly. Because it was not selected and no data was received, the return value is (false, false).

  • In the next operation, a lock is first applied with a relatively large granularity. If the channel is closed and there are no elements in the loop array buf. Corresponding to non-buffered closure and buffered closure but buf has no elements, a zero value of the corresponding type is returned, but the received flag is false, telling the caller that the channel has been closed, and the value you take out is not normally sent by the sender. The data. But if it is in the select context, this situation is selected. Many scenarios where channels are used as notification signals hit here.

  • Next, if there is a queue waiting to be sent, it means that the channel is full, either a non-buffered channel or a buffered channel, but buf is full. In both cases, data can be received normally.

So, call the recv function:

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	// 如果是非缓冲型的 channel
	if c.dataqsiz == 0 {
		if raceenabled {
			racesync(c, sg)
		}
		// 未忽略接收的数据
		if ep != nil {
			// 直接拷贝数据,从 sender goroutine -> receiver goroutine
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
		// 缓冲型的 channel,但 buf 已满。
		// 将循环数组 buf 队首的元素拷贝到接收数据的地址
		// 将发送者的数据入队。实际上这时 recvx 和 sendx 值相等
		// 找到接收游标
		qp := chanbuf(c, c.recvx)
		// …………
		// 将接收游标处的数据拷贝给接收者
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}

		// 将发送者数据拷贝到 buf
		typedmemmove(c.elemtype, qp, sg.elem)
		// 更新游标值
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.sendx = c.recvx
	}
	sg.elem = nil
	gp := sg.g

	// 解锁
	unlockf()
	gp.param = unsafe.Pointer(sg)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}

	// 唤醒发送的 goroutine。需要等到调度器的光临
	goready(gp, skip+1)
}

If it is non-buffered, it is copied directly from the sender's stack to the receiver's stack.

func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	// dst is on our stack or the heap, src is on another stack.
	src := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	memmove(dst, src, t.size)
}

Otherwise, it is a buffered channel and buf is full. It means that the sending cursor and the receiving cursor overlap, so you need to find the receiving cursor first:

// chanbuf(c, i) is pointer to the i'th slot in the buffer.
func chanbuf(c *hchan, i uint) unsafe.Pointer {
	return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

Copy the elements there to the receiving address. Then copy the data to be sent by the sender to the receiving cursor. This completes the operations of receiving data and sending data. Then, advance the sending cursor and receiving cursor by one respectively, and start from 0 if "wraparound" occurs.

Finally, take out the goroutine in sudog, call goready to change its status to "runnable", and wait for the sender to be awakened and wait for the scheduler's scheduling.

  • Then, if there is still data in the channel's buf, it means it can be received relatively normally. Note that here, you can still get here even when the channel is closed. This step is relatively simple, normally copy the data at the receiving cursor in buf to the address where the data is received.

  • At the last step, the situation leading up to this point is going to be blocked. Of course, if the value passed in by the block is false, then it will not block and just return directly.

First construct a sudog, and then save various values. Note that the address of the received data is stored in the elemfield. When woken up, the received data will be saved to the address pointed to by this field. Then add sudog to the recvq queue of the channel. Call the goparkunlock function to suspend the goroutine.

The next code is the various finishing work after the goroutine is awakened.

case analysis

We will use the following example to illustrate the process of receiving and sending data from the channel:

func goroutineA(a <-chan int) {
	val := <- a
	fmt.Println("G1 received data: ", val)
	return
}

func goroutineB(b <-chan int) {
	val := <- b
	fmt.Println("G2 received data: ", val)
	return
}

func main() {
	ch := make(chan int)
	go goroutineA(ch)
	go goroutineB(ch)
	ch <- 3
	time.Sleep(time.Second)
}

First, an unbuffered channel is created, and then two goroutines are started and the previously created channel is passed into them. Then, data 3 is sent to this channel, and the program exits after a final sleep of 1 second.

Line 14 of the program creates a non-buffered channel. We only look at some important fields in the chan structure to take a look at the status of chan from an overall level. There is nothing at the beginning:

Insert image description here

Then, lines 15 and 16 create a goroutine respectively, and each performs a receiving operation. Through the previous source code analysis, we know that these two goroutines (hereinafter referred to as G1 and G2) will be blocked in the receiving operation. G1 and G2 will hang in the recq queue of the channel, forming a two-way circular linked list.

Before line 17 of the program, the overall data structure of chan is as follows:

Insert image description here

bufPoints to an array of length 0, qcount is 0, indicating that there are no elements in the channel. Focus on recvqand sendq, they are waitq structures, and waitq is actually a two-way linked list. The element of the linked list is sudog, which contains gthe field, which grepresents a goroutine, so sudog can be regarded as a goroutine. recvq stores those goroutines that try to read the channel but are blocked, and sendq stores those goroutines that try to write to the channel but are blocked.

At this time, we can see that there are two goroutines hanging in recvq, which are G1 and G2 started earlier. Because there is no goroutine to receive, and the channel is of unbuffered type, G1 and G2 are blocked. sendq has no blocked goroutine.

recvqThe data structure is as follows:

Insert image description here

Let’s look at chan’s status as a whole:

Insert image description here

G1 and G2 are suspended, the status is WAITING. The goroutine scheduler is not the focus today. Of course, I will definitely write related articles later. Let me briefly mention here that goroutine is a user-mode coroutine and is managed by the Go runtime. In contrast, kernel threads are managed by the OS. Goroutines are more lightweight, so we can easily create tens of thousands of goroutines.

A kernel thread can manage multiple goroutines. When one of the goroutines is blocked, the kernel thread can schedule other goroutines to run, and the kernel thread itself will not block. This is what we usually call M:Na model:

Insert image description here

M:NThe model usually consists of three parts: M, P, G. M is the kernel thread, responsible for running goroutine; P is context, which saves the context required for goroutine running. It also maintains a list of runnable goroutines; G is the goroutine to be run. M and P are the basis for G operation.

Insert image description here

Let's go back to the example. Suppose we only have one M. When G1 ( go goroutineA(ch)) runs to val := <- a, it changes from the original running state to the waiting state (the result after calling gopark):

Insert image description here

G1 is separated from M, but the scheduler will not let M idle, so it will schedule another goroutine to run:

Insert image description here

The same thing happened to G2. Now both G1 and G2 are suspended, waiting for a sender to send data to the channel before they can be rescued.

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132795266