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 先进先出队列, 出会一直阻塞到有数据, 进时当队列未满不会阻塞, 队列已满则阻塞.