Go语言中有个概念叫作goroutine, 这相似咱们熟知的线程,可是更轻。
如下的程序,咱们串行地去执行两次loop函数:python
package main import "fmt" func main() { loop() loop() } func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } }
毫无疑问,输出会是这样的:shell
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
下面咱们把一个loop放在一个goroutine里跑,咱们可使用关键字go来定义并启动一个goroutine,屡次运行:编程
0 1 2 3 4 5 6 7 8 9 //或有多是下面这样 0 1 2 3 4 5 6 7 8 9 0 1 2 //亦或是下面这样 0 1 2 3 4 5 6 7 8 9 0
咱们反复运行上面的代码会发现结果会相似于上面这样,可是就是没法完整输出两遍0~9,明明咱们主线跑了一趟,也开了一个goroutine来跑一趟啊。缓存
原来,在goroutine还没来得及跑loop的时候,主函数已经退出了,正所谓"皮(主线程)之不存,毛(子线程)将焉附"。安全
main函数退出地太快了,咱们要想办法阻止它过早地退出,一个办法是让main等待一下:并发
package main import ( "fmt" "time" ) func main() { go loop() //启动一个goroutine loop() time.Sleep(time.Second) //停顿一秒 } func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } }
此次确实输出了两趟,目的达到了。
但是采用等待的办法并很差,若是goroutine在结束的时候,告诉下主线说“Hey, 我要跑完了!”就行了, 即所谓阻塞主线的办法,回忆下咱们Python里面等待全部线程执行完毕的写法:函数
for thread in threads: thread.join()
是的,咱们也须要一个相似join的东西来阻塞住主线。那就是信道oop
信道是什么?简单说,是goroutine之间互相通信的东西。相似咱们Unix上的管道(能够在进程间传递消息), 用来goroutine之间发消息和接收消息。其实,就是在作goroutine之间的内存共享。
使用make来创建一个信道:测试
var channel chan int = make(chan int)
那如何向信道存消息和取消息呢? 一个例子:ui
package main import "fmt" func main() { var messages = make(chan string) go func(message string) { messages <- message //存消息 }("Ping!") fmt.Println(<-messages) //取消息 }
默认的,信道的存消息和取消息都是阻塞的 (叫作无缓冲的信道,不过缓冲这个概念稍后了解,先说阻塞的问题)。
也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另外一端已经准备好。
好比如下的main函数和foo函数:
package main var ch = make(chan int) func foo() { ch <- 0 //向ch中加数据,若是没有其余goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走 } func main() { go foo() <-ch //从ch取数据,若是ch中还没放数据,那就挂起main线,直到foo函数中放数据为止 }
那既然信道能够阻塞当前的goroutine, 那么回到上一部分「goroutine」所遇到的问题「如何让goroutine告诉主线我执行完毕了」 的问题来, 使用一个信道来告诉主线便可:
package main import "fmt" var ch = make(chan int) func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } ch <- 0 //执行完毕了,发个消息 } func main() { go loop() <- ch //main在此阻塞住,直到线程跑完, 取到消息. }
若是不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行
其实,无缓冲的信道永远不会存储数据,只负责数据的流通,为何这么讲呢?
因此,你能够测试下,不管如何,咱们测试到的无缓冲信道的大小都是0 (len(channel))
若是信道正有数据在流动,咱们还要加入数据,或者信道干涩,咱们一直向无数据流入的空信道取数据呢? 就会引发死锁
死锁一个死锁的例子:
package main func main() { ch := make(chan int) <-ch //阻塞main goroutine,信道ch被锁 }
执行这个程序你会看到Go报这样的错误:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /Users/XXX/Go/src/main.go:6 +0x4d Process finished with exit code 2
何谓死锁? 操做系统有讲过的,全部的线程或进程都在等待资源的释放。如上的程序中, 只有一个goroutine, 因此当你向里面加数据或者存数据的话,都会锁死信道, 而且阻塞当前 goroutine, 也就是全部的goroutine(其实就main线一个)都在等待信道的开放(没人拿走数据信道是不会开放的),也就是死锁咯。
我发现死锁是一个颇有意思的话题,这里有几个死锁的例子:
1.只在单一的goroutine里操做无缓冲信道,必定死锁。好比你只在main函数里操做信道:
package main import "fmt" func main() { ch := make(chan int) ch <- 1 //1流入信道,堵塞当前线, 没人取走数据信道不会打开 fmt.Println("This line code won`t run") //在此行执行以前Go就会报死锁 }
2.以下也是一个死锁的例子:
package main import "fmt" var ch1 = make(chan int) var ch2 = make(chan int) func say(s string) { fmt.Println(s) ch1 <- <- ch2 //ch1等待ch2流出的数据 } func main() { go say("hello") <-ch1 //堵塞主线 }
其中主线等ch1中的数据流出,ch1等ch2的数据流出,可是ch2等待数据流入,两个goroutine都在等,也就是死锁。
3.其实,总结来看,为何会死锁?非缓冲信道上若是发生了流入无流出,或者流出无流入,也就致使了死锁。或者这样理解 Go启动的全部goroutine里的非缓冲信道必定要一个线里存数据,一个线里取数据,要成对才行 。因此下面的示例必定死锁:
package main func main() { c, quit := make(chan int), make(chan int) go func() { c <- 1 //c通道的数据没有被其余goroutine读取走,堵塞当前goroutine quit <- 0 //quit始终没有办法写入数据 }() <- quit //quit等待数据的写 }
仔细分析的话,是因为:主线等待quit信道的数据流出,quit等待数据写入,而func被c通道堵塞,全部goroutine都在等,因此死锁。
简单来看的话,一共两个线,func线中流入c通道的数据并无在main线中流出,确定死锁。
可是,是否果然 全部不成对向信道存取数据的状况都是死锁?
以下是个反例:
package main func main() { c := make(chan int) go func() { c <- 1 }() }
程序正常退出了,很简单,并非咱们那个总结不起做用了,仍是由于一个让人很囧的缘由,main又没等待其它goroutine,本身先跑完了, 因此没有数据流入c信道,一共执行了一个goroutine, 而且没有发生阻塞,因此没有死锁错误。
那么死锁的解决办法呢?
最简单的,把没取走的数据取走,没放入的数据放入, 由于无缓冲信道不能承载数据,那么就赶忙拿走!
具体来说,就死锁例子3中的状况,能够这么避免死锁:
package main func main() { c, quit := make(chan int), make(chan int) go func() { c <- 1 //c通道的数据没有被其余goroutine读取走,堵塞当前goroutine quit <- 0 //quit始终没有办法写入数据 }() <- c //取走c的数据 <- quit //quit等待数据的写 }
另外一个解决办法是缓冲信道, 即设置c有一个数据的缓冲大小:
c := make(chan int, 1)
这样的话,c能够缓存一个数据。也就是说,放入一个数据,c并不会挂起当前线, 再放一个才会挂起当前线直到第一个数据被其余goroutine取走, 也就是只阻塞在容量必定的时候,不达容量不阻塞。
无缓冲信道的数据进出顺序咱们已经知道,无缓冲信道从不存储数据,流入的数据必需要流出才能够。
观察如下的程序:
package main import "fmt" var ch chan int = make(chan int) func foo(id int) { ch <- id } func main() { //开启5个routine for i := 0; i < 5; i++ { go foo(i) } //取出信道中的数据 for i := 0; i < 5; i++ { fmt.Print(<- ch) } }
们开了5个goroutine,而后又依次取数据。其实整个的执行过程细分的话,5个线的数据 依次流过信道ch, main打印之, 而宏观上咱们看到的即 无缓冲信道的数据是先到先出,可是 无缓冲信道并不存储数据,只负责数据的流通
终于到了这个话题了, 其实缓存信道用英文来说更为达意: buffered channel.
缓冲这个词意思是,缓冲信道不只能够流通数据,还能够缓存数据。它是有容量的,存入一个数据的话 , 能够先放在信道里,没必要阻塞当前线而等待该数据取走。
当缓冲信道达到满的状态的时候,就会表现出阻塞了,由于这时不再能承载更多的数据了,「大家必须把 数据拿走,才能够流入数据」。
在声明一个信道的时候,咱们给make以第二个参数来指明它的容量(默认为0,即无缓冲):
var ch chan int = make(chan int, 2) // 写入2个元素都不会阻塞当前goroutine, 存储个数达到2的时候会阻塞
以下的例子,缓冲信道ch能够无缓冲的流入3个元素:
package main func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 }
若是你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。
也就是说,缓冲信道会在满容量的时候加锁。
其实,缓冲信道是先进先出的,咱们能够把缓冲信道看做为一个线程安全的队列:
package main import "fmt" func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) //1 fmt.Println(<-ch) //2 fmt.Println(<-ch) //3 }
信道数据读取和信道关闭你也许发现,上面的代码一个一个地去读取信道简直太费事了,Go语言容许咱们使用range来读取信道:
package main import "fmt" func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) } }
若是你执行了上面的代码,会报死锁错误的,缘由是range不等到信道关闭是不会结束读取的。也就是若是 缓冲信道干涸了,那么range就会阻塞当前goroutine, 因此死锁咯。
那么,咱们试着避免这种状况,比较容易想到的是读到信道为空的时候就结束读取:
package main import "fmt" func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) if len(ch) <= 0 { //若是如今数据量为0,跳出循环 break } } }
以上的方法是能够正常输出的,可是注意检查信道大小的方法不能在信道存取都在发生的时候用于取出全部数据,这个例子 是由于咱们只在ch中存了数据,如今一个一个往外取,信道大小是递减的。
另外一个方式是显式地关闭信道:
package main import "fmt" func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 //显式地关闭信道 close(ch) for v := range ch { fmt.Println(v) } }
被关闭的信道会禁止数据流入, 是只读的。咱们仍然能够从关闭的信道中取出数据,可是不能再写入数据了。
等待多gorountine的方案那好,咱们回到最初的一个问题,使用信道堵塞主线,等待开出去的全部goroutine跑完。
这是一个模型,开出不少小goroutine, 它们各自跑各自的,最后跑完了向主线报告。
咱们讨论以下2版本的方案:
对于方案1, 示例的代码大概会是这个样子:
package main import "fmt" var quit chan int //只开一个信道 func foo(id int) { fmt.Println(id) quit <- 0 //ok,finished } func main() { count := 1000 quit = make(chan int) //无缓冲 for i := 0; i < count; i++ { go foo(i) } for i := 0; i < count; i++ { <- quit } }
对于方案2,把信道换成1000的:
quit = make(chan int, count) // 容量1000
其实区别仅仅在于一个是缓冲的,一个是非缓冲的。
对于这个场景而言,二者都能完成任务, 都是能够的。
转载:
Go编程基础—并发