Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go语言中的通道(channel)是一种特殊的类型,通道像一个传送带或队列,总是遵循先进先出(First In First Out)的规则,保证收发数据的顺序,每一个通道就是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。
一、 channel 的声明方式
-
var ch chan T 此时的
ch
值为nil
,channel的一个特性是向值为nil
的channel发送数据会阻塞 -
ch := new(chan T) 同上使用
var
关键字,ch的值是nil
-
ch := make(chan T) ch 是一个非空的指针,此时可以向 ch 发送或者接收数据
因此,第一种和第二种虽然可以声明,但返回值无法使用,一定会要通过 make
关键字声明的值才可用。
|
|
二、channel的类型
根据缓冲区区分
- 无缓冲区channel:无缓冲区channel的容量为0,即无缓冲区,发送数据时,如果接收端没有准备好,则阻塞,直到接收端准备好接收数据。
- 有缓冲区channel:有缓冲区channel的容量大于0,即有缓冲区,发送数据时,如果缓冲区已满,则阻塞,直到接收端接收数据后缓冲区有空间。
根据读写类型
chan
可读可写<-chan
只读chan<-
只写
channel 的基本操作和注意事项
- channel的3种状态
-
- nil, 未初始化的状态,只进行了声明,或手动赋值为nil
-
- active, 正常的channel,可读或者可写
-
- closed, 已关闭的channel,【注意】已关闭的channel值不是nil,在select 中可以判断是否关闭,但无法判断是否为nil,而且已关闭的channel依然可以读,只是读出的数据是零值。只有将关闭的channel赋值为nil,才可以停止接收。
channel的三种操作
1. 读操作(接收数据)
- 从
nil
值的chan读取数据会阻塞
|
|
- 从正常的channel读取数据,如果缓冲区为空,则阻塞,直到缓冲区有数据
|
|
- 读取已关闭的 channel,读取到的是零值
|
|
写操作(发送数据)
- 向
nil
值的chan发送数据会阻塞
|
|
- 向正常的channel发送数据,如果缓冲区已满,则阻塞,直到缓冲区有空间
|
|
- 向已关闭的channel发送数据,会panic
|
|
关闭
- 关闭 nil 的channel,会导致panic
|
|
- 关闭正常channel,成功
|
|
- 关闭已关闭的channel,会导致panic
|
|
针对 channel
的三种状态和三种操作,可以组合成九种情况如下:
操作 | nil的channel | 正常的channel | 已关闭的channel |
---|---|---|---|
读 | 阻塞 | 成功或阻塞 | 正常(返回零值) |
写 | 阻塞 | 成功或阻塞 | panic |
关闭 | panic | 成功 | panic |
三、channel 如何实现线程安全?
Go中的channel是一种用于不同goroutine之间通信的原语,它可以在多个goroutine之间安全地传递数据,而不需要显式地使用锁机制(如mutex)来同步访问。Go语言的设计确保了channel在并发场景下是安全的,这使得它非常适合在多goroutine环境中用于数据传递和同步。
- 锁机制
在channel的底层实现中,所有对channel的操作(包括发送、接收、关闭等)都会被加锁,以防止多个goroutine同时操作channel时出现数据竞争。Go runtime为每个channel分配了一个mutex锁来保护channel的状态,从而保证了在多goroutine并发操作时的线程安全性。
- Goroutine调度与阻塞
当一个goroutine因为channel满了(发送方)或channel空了(接收方)而被阻塞时,Go的调度器会将该goroutine挂起,放入对应的队列(sendq或recvq)。一旦条件满足(比如有接收者准备好接收数据),被阻塞的goroutine会被唤醒继续执行
- 关闭channel的安全性
关闭一个channel时,所有在等待接收该channel的goroutine都会被立即唤醒,并且它们会收到零值,从而安全退出。此外,尝试向已关闭的channel发送数据会引发panic,这是Go语言的一种安全机制,避免意外的并发问题。
四、channel的底层实现总结
- channel使用 锁(mutext) 来确保线程安全,防止数据竞争。
- channel通过 goroutine调度和队列 实现阻塞和唤醒机制,使得多个goroutine可以安全的发送和接收数据。
- 无缓冲的channel是同步的,而有缓冲的channel是异步的,二者在实现机制上有所不同。
- Go的调度器负责管理阻塞的goroutine,使得程序不会因为阻塞而卡死。
五、channel 的发送和接收数据过程实现
无缓冲区的channel
- 发送数据流程
- 加锁,发送方调用
ch <- value
,Go runtime 会对channel加锁,防止其他 goroutine 同时操作channel。 - 检查接收队列,Go runtime 会首先检查
revq
(接收队列)是否有等待的接收方。
- 如果有,则从队列中取出一个接收方,并唤醒它。
- 如果没有,则将发送方加入到发送队列
sendq
中。
- 解锁,发送操作结束后,Go runtime 会解锁,允许其他 goroutine 操作channel。
- 接收数据流程
- 加锁,接收方调用
value := <-ch
,Go runtime 会对channel加锁,防止其他 goroutine 同时操作channel。 - 检查发送队列,Go runtime 会首先检查
sendq
(发送队列)是否有等待的发送方。
- 如果有,则从队列中取出一个发送方,并唤醒它。
- 如果没有,则将接收方加入到接收队列
recvq
中。
- 解锁,接收操作结束后,Go runtime 会解锁,允许其他 goroutine 操作channel。
- 无缓冲channel操作的总结
- 如果发送方先到,且没有接收方,发送方阻塞并进入
sendq
。- 如果接收方先到,且没有发送方,接收方阻塞并进入
recvq
。- 如果发送方和接收方匹配成功后,Go runtime 会进行数据交换,并唤醒被阻塞的 goroutine。
有缓冲区的channel
有缓冲的 channel 不需要发送和接收操作严格同步,发送方可以在缓冲区未满时发送数据,而不阻塞。接收方可以在缓冲区中有数据时接收数据,而不等待。
发送数据流程
- 加锁,发送方调用
ch <- value
,Go runtime 加锁,防止其他 goroutine 并发操作 channel。 - 检查缓冲区:
- 如果缓冲未满,则将数据写入缓冲区,
sendx
(发送索引)递增。- 如果缓冲已满,则阻塞发送方,并将发送方加入到发送队列
sendq
中。
- 解锁,发送操作结束后,Go runtime 解锁,允许其他 goroutine 操作 channel。
接收数据流程
- 加锁,接收方调用
value := <-ch
,Go runtime 加锁,防止其他 goroutine 并发操作 channel。 - 检查缓冲区:
- 如果缓冲中有数据,则从缓冲区中取出数据,
recvx
(接收索引)递增。- 如果缓冲为空,则阻塞接收方,并将接收方加入到接收队列
recvq
中。
- 解锁,接收操作结束后,Go runtime 解锁,允许其他 goroutine 操作 channel。
有缓冲channel操作的总结
- 如果缓冲未满,则发送方直接写入缓冲区。
- 如果缓冲区已满,发送方阻塞并进入
sendq
。- 如果缓冲中有数据,则接收方直接从缓冲区中读取数据而不阻塞。
- 如果缓冲为空,接收方阻塞并进入
recvq
。