无缓冲阻塞 chan 杂谈
chan 类似队列版管道,无缓冲 chan 看起来好像是全局变量,通过它可让多个 goroutine 间通信。 这其实隐含一个事实,chan 阻塞会引发 goroutine 上下文切换,而切换到哪一个可执行 goroutine 由 go 调度器决定(与阻塞 chan 相关)。go 当前能够使用的 goroutine,必须在其待命队列中,否则会产生死锁。
上下文切换#
多进程多线程都具备上下文切换,即保存恢复现场的能力。goroutine 的上下文切换实现,是在用户态基础上进行,只不过它涉及到的资源比线程更少,如产生一个线程系统调用分配内存通常在 1M,而 goroutine 只有 2kb,此外在使用寄存器,段位上,goroutine 也只需 3 个左右,而线程则通常在 10 个左右。
无缓冲阻塞#
go 调度器对 goroutine 的使用配合 chan,具有有序性(在高并发访问对象时,可用 chan 这种特性让访问请求隐性排队,解决竞态问题)。main 函数是特殊的入口 goroutine,若有阻塞代码,运行时 runtime 会寻找已入队列的 goroutine 并在适当的时机调用它。chan 并不是全局变量,确切来说它的读 / 写阻塞会触发当前 goroutine 执行权转移,它只是个通信器。好似打电话,必须先知道对方号码并有连线,才能正常工作,若顺序不对,表现在 golang 中便是死锁
Blocking#
package main
import (
"fmt"
)
func f1(in chan int) {
fmt.Println(<-in)
}
func main() {
out := make(chan int)
out <- 2
go f1(out)
}
上述代码会产生死锁,main 入口 goroutine,通道 out 产生了发送阻塞,此时 runtime 会尝试调度与 out 通道读相关的 goroutine 执行,但可惜的是,在 out <- 2
之前,并没有向 go 执行器队列加入与 out 读相关的 goroutine。换句话而言,f1 压根就没入队,没有执行机会。
unblocking#
package main
import "fmt"
func main() {
out := make(chan int)
go f1(out)
// 此处顺序大有讲究,在使用发送通道之前必需想好数据接收的退路,f1即是
out <- 2
}
func f1(in chan int) {
fmt.Println(<-in)
}
chan vs 全局变量#
上文提到 chan 类似管道,管道顾名思义一端进一端出,很形象表明了一个连接器。go 中的 chan 连接 goroutine,游离于众多 goroutine 之间,功用性与全局变量有得一拼。但 chan 绝对不是全局变量,一个全局变量,可以在同一函数体内重复读写,但对无缓冲 chan 而言是不可以,原因在同一 goroutine 内对同一 chan 读写时,存在读或写阻塞面临切换上下文,另一个对应的永远没执行机会,如下
- 无缓冲通道死锁
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 5
fmt.Println(<-ch)
}
- 有缓冲通道正常
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 5
fmt.Println(<-ch)
}
有缓冲通道,意味着在未超过当前通道限制数之前,当前的 goroutine 是非阻塞,不会发生上下文切换,即当前 goroutine 的控制权不发生转移,runtime 也就不会去寻求其它相关 goroutine 执行。
小结#
- 无缓冲 chan 进和出都会阻塞.
- 有缓冲 chan 先进先出队列,出会一直阻塞到有数据,进时当队列未满不会阻塞,队列已满则阻塞.
本作品采用《CC 协议》,转载必须注明作者和本文链接