8. Goroutine和Channel
这两个的实现是Go大受欢迎的原因。在其他的一些主流语言,实现线程占用内存资源比较大还有线程之间的通讯必须通过复杂的加锁机制来实现。Goroutine和Channel的出现就是为了解决这一个问题。
- Goroutine是由官方实现的超级“线程池”,每一个实例4-5kb的栈内存占用和由于实现机制而大幅度减少和创建和销毁开销,是Go号称高并发的根本原因。
- 并发不是并行:并发主要由切换时间片来实现“同时”运行,而并行则是直接利用多核实现多线程运行,但Go可以设置使用核数,以发挥多核计算机的能力。
- Go奉行通过通信来共享内存,而不是共享内存来通信。
Goroutine
Goroutine翻译过来应该叫做协程,或者叫做微线程或者用户态轻量级线程。一个协程或者多个协程对应着一个线程,协程的实现实在用户态中,这就避免了要陷入内核态进行操作,节省了不少时间。具体可以看看这篇《Golang:线程 和 协程 的区别》
在Go中实现一个协程是很简单的,只需要在函数之前使用‘‘ go “就好了,比如:
func Hello() {
fmt.Println("Hello World in func")
}
func main() {
go Hello() // 对hello函数创建协程
fmt.Println("Hello world in main")
}
但是运行以上代码会发现只输出了Hello world in main
为了解释这个原因,我们先可以这样main是一个主协程,它可以控制由其产生的协程。另外协程的运作方式是时间片运作方式,也就是一个协程运行一段时间,然后交个其他协程运行(这也是并行的原理)。
而在上面的代码段中,主协程由于进行的操作很少,一个时间片就能运行完,所以Hello这个协程还没来得及运行主协程就结束了。而主协程是整个程序的主体,一旦结束其他的所有操作也就会结束,包括Hello这个协程。
那么,怎么办呢?最简单粗暴的方法就是让主协程“等”一会,等Hello这个协程运行完再结束。使用time.Sleep让主协程“睡”一会:
func main() {
go Hello()
time.Sleep(time.Second) // 等待一秒
fmt.Println("Hello world in main")
}
这样一来,就可以得到我们想要的结果了:
Hello World in func
Hello world in main
但是上面的程序有一个很大的问题,就是主协程等待的时间很有可能太长了或者我们很难控制主协程要等待的时间。比如Hello这个协程仅运行了0.1s(假设而已),而主协程却等待了1s,那么就有0.9s的时间浪费了。
那么有没有一种机制来告诉主协程,Hello协程运行完了呢?答案就是Channel
Channel
特性:
- Channel是Goroutine沟通的桥梁,大都是阻塞同步的
- 通过make创建,close关闭
- Channel是引用类型
- 可以使用for range类不断迭代操作Channel
- 可以设置单向或者双向通道
- 可以设置缓存大小,在未被填满前不会发生阻塞
- 永远不要在关闭的通道写数据
Channel翻译过来就叫做通道或者消息通道。我们把协程比做人,那么Channel就是快递员(送信员也可以)。
通过make创建,close关闭:
c := make(chan, bool) // 创建bool类型的通道,当然也可以是其他类型。
//c := make(chan, bool, 2) // 创建容量为2的通道,也叫做异步通道
close(c)
用通道改写上一小节的代码,实现协程间的通讯:
func main() {
c := make(chan bool)
// 为了方便使用c,将Hello函数改写为匿名函数
go func() {
fmt.Println("Hello World in func")
c <- true
}()
<- c
fmt.Println("Hello World in main")
}
上面的代码执行的效果和上一节最后一个代码是一样的。
在主协程中<- c
表示在通道中取出数据,但是通道如果没有数组取出来,那么将会一直阻塞(等待),直到从通道中顺利取出一个数据为止。而匿名函数实现的协程一旦将数据写入c
,主协程就会立刻被通知可以取数据,取出数据之后便可以顺利的往下执行其他操作了。这就实现了两个协程之间的操作。
可以使用for range类不断迭代操作Channel:
for range操作Channel要注意的是,必须有要手动关闭通道,不然会造成阻塞:
func main() {
c := make(chan bool)
// 为了方便使用c,将Hello函数改写为匿名函数
go func() {
fmt.Println("Hello World in func")
c <- true
}()
for v := range c {
fmt.Println(v)
}
fmt.Println("Hello World in main")
}
结果:
Hello World in func
true
fatal error: all goroutines are asleep - deadlock!
可以看到造成死锁(deadlock)了,也就是for range 一直在等待c传输数据过来,但是我们的匿名函数协程已经运行完毕,不会继续往通道中写数据了。
为了解决这个问题,一定要手动关闭通道,告诉for range通道关闭了,停止继续去参数的操作:
func main() {
c := make(chan int)
go func() {
fmt.Println("Hello World in func")
c <- 10
close(c)
}()
for v := range c {
fmt.Println(v)
}
fmt.Println("Hello World in main")
}
结果:
Hello World in func
10
Hello World in main
可以设置单向或者双向通道
我们在将通道传递给一个函数的时候,有时只需要往通道里面写数据,有时只需要读数据。那么我们就可以声明一个单向通道,只允许读或写的操作。当然我们也可以定义一个双向通道。
首先双向通道的声明和我们之前学得是一样的 var ch chan int
func main() {
var ch = make(chan int)
go Go(ch)
ch <- 20
get := <- ch
fmt.Println(get)
}
func Go(ch chan int) {
temp := <- ch
temp++
ch <- temp
}
可以用<-
和->
来声明通道的可读性或者可写性。
var wg = sync.WaitGroup{}
func main() {
var ch = make(chan int)
wg.Add(2)
go test1(ch)
go test2(ch)
wg.Wait()
}
func test1(ch <-chan int) {
temp := <- ch
fmt.Println(temp)
//ch <- 21 // 错误
wg.Done()
}
func test2(ch chan<- int) {
ch <- 20
//temp := <-ch // 错误
wg.Done()
}
上述代码中加入WaitGroup进行流程控制,避免主协程过快停止运行。
永远不要在关闭的通道写数据
在关闭的通道写数据会引发panic:
func main() {
c := make(chan int)
for i:=0; i<10; i++ {
go func(a int) {
c <- a
if a == 9 {
close(c)
}
}(i)
}
for v := range c {
fmt.Printf("%d ", v)
}
fmt.Println("In Main")
}
由于协程是随机调度的,所以有可能10个协程不是按顺序执行的,如果a==9
在有的数字还没执行前执行了,并且关闭了通道,那么很有可能其他协程在写数据的时候会报错。
Selet
- select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
- 可以处理一个或者多个Channel的发送和接收
- 同时有多个可用的Channel时按随机顺序处理
- 可用空的select来阻塞main函数
- 可以设置超时
select一般和case配合使用,并且case是随机选择执行的。
处理两个channel:
func main() {
c1, c2 := make(chan int), make(chan string)
o := make(chan bool)
go func() {
for {
select {
case v, ok := <- c1:
if !ok {
o <- false
break
}
fmt.Println("c1", v)
case v, ok := <- c2:
if !ok {
o <- false
break
}
fmt.Println("c1", v)
}
}
}()
c1 <- 10
c2 <- "hello"
c1 <- 20
c2 <- "world"
close(c1)
close(c2)
for i:=0; i<2; i++ {
<- o
}
}
多个channel同时准备好读的情况:
func TestMultiChannel() {
c1 := make(chan interface{}); close(c1)
c2 := make(chan interface{}); close(c2)
c3 := make(chan interface{}); close(c3)
var c1Count, c2Count, c3Count int
for i := 1000; i >= 0; i-- {
select {
case <-c1:
c1Count++
case <-c2:
c2Count++
case <-c3:
c3Count++
}
}
fmt.Printf("c1Count: %d\nc2Count: %d\nc3Count: %d\n", c1Count, c2Count, c3Count)
}
输出:
c1Count: 337
c2Count: 319
c3Count: 345
多运行几次,可以看出,几个数字相差都不是很大。
以上例子,同时有3个channel可读取,从以上的输出可以看出,select对多个channel的读取调度是基本公平的。让每一个channel的数据都有机会被处理。
没有任何channel准备好,处理超时:
在很多情况下,当channel没有准备好时,我们希望能够设置一个超时时间,并在等待channel超时时进行一些处理。此时就可以按以下方式来进行编码:
func main() {
c, o := make(chan int), make(chan bool)
go func() {
for {
select {
case v := <- c :
fmt.Println(v)
o <- true
return
case <- time.After(2*time.Second): // 等待两秒没有消息从通道过来打印一下
fmt.Println("Wait 2 seconds")
}
}
}()
time.Sleep(6*time.Second)
c <- 10
<- o // 阻塞,避免主协程提早停止
}
结果:
After
Push
After
After
After
没有任何channel准备好,处理默认事件:
func TestDefaultProc() {
start := time.Now()
var c1, c2 <-chan int
select {
case <-c1:
case <-c2:
default:
fmt.Printf("In default after %v\n\n", time.Since(start))
}
}
注意:default和处理超时不同,当没有channel可读取时,会立即执行default分支。而超时的处理,必须要等到超时,才处理。
通过channel通知,从而退出死循环:
func TestExitLoop() {
done := make(chan interface{})
go func() {
time.Sleep(2*time.Second)
close(done)
}()
workCounter := 0
loop:
for {
select {
case <-done:
break loop
default:
}
// Simulate work
workCounter++
time.Sleep(1*time.Second)
}
fmt.Printf("在通知退出循环时,执行了%d次.\n", workCounter)
}
**永久等待:**永远等待,直到有信号中断。
select{}