La esencia de la entrevista en idioma GO: ¿cuál es el proceso de recepción de datos del canal?

Análisis de código fuente

Primero echemos un vistazo al código fuente relacionado con la recepción. Tras aclarar el proceso concreto de recepción, lo estudiaremos en detalle a partir de un ejemplo práctico.

Hay dos formas de escribir la operación de recepción, una es con "ok", que refleja si el canal está cerrado, la otra es sin "ok", de esta manera cuando se recibe un valor cero del tipo correspondiente, se imposible saber si fue enviado por el remitente real, o un valor cero del tipo predeterminado devuelto al receptor después de cerrar el canal. Ambos métodos de escritura tienen sus propios escenarios de aplicación.

Después del procesamiento por parte del compilador, estos dos métodos de escritura finalmente corresponden a estas dos funciones en el código fuente:

// 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
}

chanrecv1Si la función maneja la situación sin "ok", chanrecv2devolverá el campo "recibido" para reflejar si el canal está cerrado. El valor recibido es especial y se "colocará" elemen la dirección señalada por el parámetro, que es muy similar al método de escritura en C/C++. Si el valor recibido se omite en el código, elem aquí es nulo.

De todos modos, finalmente recurrimos a chanrecvlas funciones:

// 位于 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
}

El código anterior se comenta con más detalle. Puede mirar el código fuente línea por línea. Echemos un vistazo más de cerca.

  • Si el canal es un valor nulo (nulo), se devolverá directamente en modo sin bloqueo. En el modo de bloqueo, se llamará a la función gopark para suspender la rutina, que continuará bloqueando. Porque cuando el canal es nulo, la única forma de evitar el bloqueo es cerrarlo, sin embargo, cerrar un canal nulo volverá a entrar en pánico, por lo que no hay posibilidad de que lo despierten. Puedes ver la función closechan con más detalle.

  • Al igual que la función de envío, a continuación realizamos una operación que detecta rápidamente la falla y regresa al modo sin bloqueo sin adquirir un bloqueo. Por cierto, cuando normalmente escribimos código, encontramos algunas condiciones límite y regresamos rápidamente para hacer que la lógica del código sea más clara, porque habrá menos situaciones normales y estará más enfocada, y las personas que leen el código estarán más enfocadas. Mire la lógica del código central.

	// 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回 (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
	}

Cuando observamos que el canal no está listo para recibir:

  1. Sin búfer, no hay rutinas esperando en la cola de envío.
  2. Buffered, pero no hay elementos en buf

Posteriormente se observó que cerrado == 0, es decir, el canal no estaba cerrado.

Debido a que el canal no se puede abrir repetidamente, el canal no se cerró durante la observación anterior, por lo que en este caso puede declarar directamente la falla de recepción y regresar rápidamente. Debido a que no se seleccionó y no se recibieron datos, el valor de retorno es (falso, falso).

  • En la siguiente operación, primero se aplica un bloqueo con una granularidad relativamente grande. Si el canal está cerrado y no hay elementos en la matriz del bucle buf. Correspondiente al cierre sin búfer y al cierre con búfer, pero buf no tiene elementos, se devuelve un valor cero del tipo correspondiente, pero el indicador recibido es falso, lo que le indica a la persona que llama que el canal se ha cerrado y que el valor que usted extrae no lo es. normalmente enviado por el remitente.Los datos. Pero si es en el contexto seleccionado, se selecciona esta situación. Muchos escenarios en los que se utilizan canales como señales de notificación llegan aquí.

  • A continuación, si hay una cola esperando ser enviada, significa que el canal está lleno, ya sea un canal sin búfer o un canal con búfer, pero buf está lleno. En ambos casos, los datos se pueden recibir normalmente.

Entonces, llama a la función recv:

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)
}

Si no está almacenado en el búfer, se copia directamente de la pila del remitente a la pila del receptor.

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)
}

De lo contrario, es un canal almacenado en búfer y el buf está lleno. Significa que el cursor de envío y el cursor de recepción se superponen, por lo que primero debe encontrar el cursor de recepción:

// 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))
}

Copie los elementos allí a la dirección de recepción. Luego copie los datos que enviará el remitente al cursor de recepción. Esto completa las operaciones de recibir y enviar datos. Luego, avance el cursor de envío y el cursor de recepción uno respectivamente, y comience desde 0 si se produce un "cambio".

Finalmente, elimine la rutina en sudog, llame a goready para cambiar su estado a "ejecutable" y espere a que se despierte el remitente y espere la programación del programador.

  • Entonces, si todavía hay datos en el buf del canal, significa que se pueden recibir con relativa normalidad. Tenga en cuenta que aquí aún puede llegar incluso cuando el canal esté cerrado. Este paso es relativamente simple, normalmente copia los datos en el cursor de recepción en buf a la dirección donde se reciben los datos.

  • En el último paso, se bloqueará la situación que conduce a este punto. Por supuesto, si el valor pasado por el bloque es falso, entonces no se bloqueará y simplemente regresará directamente.

Primero construya un sudog y luego guarde varios valores. Tenga en cuenta que la dirección de los datos recibidos se almacena en el elemcampo. Cuando se despierte, los datos recibidos se guardarán en la dirección señalada por este campo. Luego agregue sudog a la cola de recepción del canal. Llame a la función goparkunlock para suspender la rutina.

El siguiente código son los diversos trabajos de finalización después de que se despierta la rutina.

analisis de CASO

Usaremos el siguiente ejemplo para ilustrar el proceso de recepción y envío de datos desde el canal:

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)
}

Primero, se crea un canal sin búfer y luego se inician dos gorutinas y se les pasa el canal creado previamente. Luego, los datos 3 se envían a este canal y el programa sale después de un reposo final de 1 segundo.

La línea 14 del programa crea un canal sin búfer. Solo miramos algunos campos importantes en la estructura de chan para ver el estado de chan desde un nivel general. No hay nada al principio:

Insertar descripción de la imagen aquí

Luego, las líneas 15 y 16 crean una gorutina respectivamente y cada una realiza una operación de recepción. A través del análisis del código fuente anterior, sabemos que estas dos gorutinas (en adelante G1 y G2) se bloquearán en la operación de recepción. G1 y G2 permanecerán en la cola de recepción del canal, formando una lista enlazada circular de dos vías.

Antes de la línea 17 del programa, la estructura de datos general de chan es la siguiente:

Insertar descripción de la imagen aquí

bufApunta a una matriz de longitud 0 y qcount es 0, lo que indica que no hay elementos en el canal. Concéntrese en recvqy sendq, son estructuras de waitq, y waitq es en realidad una lista enlazada bidireccional. El elemento de la lista enlazada es sudog, que contiene gun campo que grepresenta una gorutina, por lo que sudog puede considerarse como una gorutina. recvq almacena aquellas gorutinas que intentan leer el canal pero están bloqueadas, y sendq almacena aquellas gorutinas que intentan escribir en el canal pero están bloqueadas.

En este momento, podemos ver que hay dos gorutinas colgadas en recvq, que son G1 y G2 iniciadas anteriormente. Debido a que no hay ninguna rutina para recibir y el canal no tiene búfer, G1 y G2 están bloqueados. sendq no tiene ninguna rutina bloqueada.

recvqLa estructura de datos es la siguiente:

Insertar descripción de la imagen aquí

Veamos el estado de Chan en su conjunto:

Insertar descripción de la imagen aquí

G1 y G2 están suspendidos, el estado es WAITING. El programador de rutinas no es el foco de atención hoy, por supuesto, definitivamente escribiré artículos relacionados más adelante. Permítanme mencionar brevemente aquí que goroutine es una corrutina en modo de usuario y es administrada por el tiempo de ejecución de Go. Por el contrario, los subprocesos del kernel son administrados por el sistema operativo. Las gorutinas son más livianas, por lo que podemos crear fácilmente decenas de miles de gorutinas.

Un subproceso del kernel puede administrar múltiples gorutinas. Cuando una de las gorutinas está bloqueada, el subproceso del kernel puede programar la ejecución de otras gorutinas y el subproceso del kernel en sí no se bloqueará. Esto es lo que solemos llamar M:Nmodelo:

Insertar descripción de la imagen aquí

M:NEl modelo suele constar de tres partes: M, P, G. M es el hilo del kernel, responsable de ejecutar goroutine; P es el contexto, que guarda el contexto necesario para la ejecución de goroutine. También mantiene una lista de goroutines ejecutables; G es el goroutine que se ejecutará. M y P son la base para la operación G.

Insertar descripción de la imagen aquí

Volvamos al ejemplo. Supongamos que solo tenemos una M. Cuando G1 ( go goroutineA(ch)) se ejecuta val := <- a, cambia del estado de ejecución original al estado de espera (el resultado después de llamar a gopark):

Insertar descripción de la imagen aquí

G1 está separado de M, pero el programador no dejará que M esté inactivo, por lo que programará la ejecución de otra rutina:

Insertar descripción de la imagen aquí

Lo mismo le pasó al G2. Ahora tanto G1 como G2 están suspendidos, esperando que un remitente envíe datos al canal antes de que puedan ser rescatados.

Supongo que te gusta

Origin blog.csdn.net/zy_dreamer/article/details/132795266
Recomendado
Clasificación