目录
4.4.5 使用 sync.WaitGroup 等待 goroutine 结束
介绍:
通道是 Go 编程语言的一个基本特性,它提供了协程之间通信和同步的方式。协程是轻量级线程,允许并发执行代码。通道用于在协程之间传递数据和控制流。本文将探讨通道的使用方法、通道的底层实现以及如何使用通道进行高效的并发编程。
一、通道的定义和使用方法
1.1 通道的定义
通道是一种类型,类似于一个队列。通过使用通道,我们可以在协程之间传递数据,协程会在通道上阻塞等待数据的到来,直到数据被发送到通道中,协程才会继续执行。
在 Go 中,使用 make() 函数来创建通道,语法如下:
ch := make(chan 数据类型)
其中,数据类型指定了通道可以传递的数据类型。通道可以传递任意类型的数据,包括自定义类型。
1.2 通道的使用
通道的基本操作包括发送和接收。发送操作使用 <- 运算符,接收操作使用 <- 运算符。例如:
ch := make(chan int)
ch <- 10 // 向通道 ch 发送整数 10
x := <-ch // 从通道 ch 接收一个整数,并赋值给变量 x
通道的发送和接收操作都是阻塞的。如果发送操作阻塞了,那么协程会一直阻塞,直到有另外一个协程从该通道中接收了数据。同样地,如果接收操作阻塞了,那么协程会一直阻塞,直到有另外一个协程向该通道中发送了数据。
为了防止通道死锁,我们通常需要使用协程来发送和接收数据,以便程序可以继续执行其他操作。
ch := make(chan int)
go func() {
ch <- 10
}()
x := <-ch
在上面的示例中,我们使用了一个匿名协程来发送数据,然后在主协程中接收数据。
1.3 通道的关闭
通道可以通过调用 close() 函数来关闭。关闭通道后,所有的发送操作都会失败,并且接收操作会在通道中的数据被读取完毕后自动返回零值。例如:
ch := make(chan int)
go func() {
ch <- 10
ch <- 20
ch <- 30
close(ch)
}()
for x := range ch {
fmt.Println(x)
}
在上面的示例中,我们使用了一个匿名协程向通道中发送了三个整数,并在发送完毕后关闭了通道。然后我们使用 for 循环来从通道中接收数据,直到通道被关闭。
二、通道的底层实现
2.1 通道的数据结构
通道是一个结构体类型,包含了一个指向数据队列的指针,以及发送和接收操作的相关信息。例如:
type hchan struct {
qcount uint // 数据队列中的元素数量
dataqsiz uint // 数据队列的大小
buf unsafe.Pointer // 指向数据队列的指针
elemsize uint16 // 每个元素的大小
closed uint32 // 通道是否已经关闭
recvx uint64 // 下一个接收操作的序列号
sendx uint64 // 下一个发送操作的序列号
recvq waitq // 等待接收的协程队列
sendq waitq // 等待发送的协程队列
lock mutex // 互斥锁
}
2.2 通道的发送和接收操作
通道的发送和接收操作是通过一个序列号来实现的,每个发送和接收操作都有一个唯一的序列号。序列号由两部分组成:通道的序列号和操作类型(发送或接收)。
通道的序列号用于区分不同的通道,操作类型用于区分不同的发送和接收操作。发送操作的序列号是 sendx,接收操作的序列号是 recvx。
在发送操作中,发送的数据会被复制到通道的数据队列中,并更新 sendx 的值。如果数据队列已满,则发送操作会被阻塞,直到有其他协程从该通道中读取数据。
在接收操作中,接收到的数据会从通道的数据队列中读取,并更新 recvx 的值。如果数据队列为空,则接收操作会被阻塞,直到有其他协程向该通道中发送数据。
2.3 通道的缓冲区
通道可以具有缓冲区,缓冲区用于存储已发送但未被接收的数据。缓冲区的大小在创建通道时指定,可以通过通道的 dataqsiz 字段来访问。
在带有缓冲区的通道中,发送操作不会被阻塞,除非缓冲区已满。当缓冲区已满时,发送操作将被阻塞,直到有其他协程从该通道中读取数据。接收操作与非缓冲区通道中的操作相同,当缓冲区为空时会被阻塞。
2.4 通道的等待队列
每个通道都有一个等待接收的协程队列和一个等待发送的协程队列。在发送和接收操作被阻塞时,协程将被添加到相应的等待队列中。当数据可以被发送或接收时,等待队列中的协程会被唤醒并执行。
等待队列使用 waitq 结构体来实现,如下所示:
type waitq struct {
first *sudog // 队列的第一个元素
last *sudog // 队列的最后一个元素
}
等待队列中的每个协程都会包含一个 sudog 结构体,该结构体用于存储协程的状态和等待的通道。sudog 结构体的定义如下:
type sudog struct {
g *g
selectdone uint32
next *sudog
prev *sudog
elem unsafe.Pointer
waitlink *sudog
selectdonep *uint32
}
2.5 通道的关闭
通道可以通过调用 close 函数来关闭。关闭通道后,所有的发送操作都将被阻塞,并且所有未接收的数据都将被丢弃。对于已经关闭的通道,任何发送操作都将导致 panic 错误。
通道的关闭标志位保存在 closed 字段中,该字段是一个 uint32 类型的原子变量。当通道被关闭时,将 closed 置为 1,以指示通道已经关闭。
在发送操作中,如果通道已经关闭,则会返回 panic 错误。在接收操作中,如果通道已经关闭且数据队列为空,则接收操作将返回一个零值,并返回一个布尔值,以指示通道是否已经关闭。
2.6 通道的选择器
通道选择器是一种用于在多个通道之间进行选择的机制。通道选择器通过 select 语句实现,select 语句可以同时监听多个通道的读写操作,并在其中一个操作就绪时执行相应的代码块。
select 语句的语法如下:
select {
case <- ch1:
// 处理 ch1 的接收操作
case ch2 <- value:
// 处理 ch2 的发送操作
default:
// 如果没有通道就绪,则执行默认操作
}
在 select 语句中,每个 case 语句都表示一个通道的读或写操作。在多个 case 语句中,只有一个可以被执行,具体选择哪个 case 取决于哪个操作最先就绪。如果没有通道就绪,则执行 default 语句块。
select 语句在底层使用了通道的等待队列,用于监听多个通道的读写操作。当其中一个操作就绪时,select 语句会从等待队列中找到对应的 sudog 结构体,并将对应的协程唤醒并执行。