在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据。 以下是我入门的学习笔记。 首先,并行!=并发, 两者是不同的,可以参考:http://concur.rspace.googlecode.com/hg/talk/concur.html goroutineGo语言中有个概念叫做goroutine, 这类似我们熟知的线程,但是更轻。 以下的程序,我们串行地去执行两次
毫无疑问,输出会是这样的:
func main() {
go loop() // 启动一个goroutine
loop()
}
这次的输出变成了:
原来,在goroutine还没来得及跑loop的时候,主函数已经退出了。 main函数退出地太快了,我们要想办法阻止它过早地退出,一个办法是让main等待一下: func main() {
go loop()
loop()
time.Sleep(time.Second) // 停顿一秒
}
这次确实输出了两趟,目的达到了。 可是采用等待的办法并不好,如果goroutine在结束的时候,告诉下主线说“Hey, 我要跑完了!”就好了, 即所谓阻塞主线的办法,回忆下我们Python里面等待所有线程执行完毕的写法: for thread in threads:
thread.join()
使用
那如何向信道存消息和取消息呢? 一个例子: func main() {
var messages chan string = make(chan string)
go func(message string) {
messages <- message // 存消息
}("Ping!")
fmt.Println(<-messages) // 取消息
}
默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道,不过缓冲这个概念稍后了解,先说阻塞的问题)。 也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。 比如以下的main函数和foo函数: var ch chan int = make(chan int)
func foo() {
ch <- 0 // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走
}
func main() {
go foo()
<- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止
}
那既然信道可以阻塞当前的goroutine, 那么回到上一部分「goroutine」所遇到的问题「如何让goroutine告诉主线我执行完毕了」 的问题来, 使用一个信道来告诉主线即可: var complete chan int = make(chan int)
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
complete <- 0 // 执行完毕了,发个消息
}
func main() {
go loop()
<- complete // 直到线程跑完, 取到消息. main在此阻塞住
}
如果不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行、、、 其实,无缓冲的信道永远不会存储数据,只负责数据的流通,为什么这么讲呢?
所以,你可以测试下,无论如何,我们测试到的无缓冲信道的大小都是0 ( 死锁一个死锁的例子: func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 信道c被锁
}
执行这个程序你会看到Go报这样的错误:
我发现死锁是一个很有意思的话题,这里有几个死锁的例子:
但是,是否果真 所有不成对向信道存取数据的情况都是死锁? 如下是个反例: func main() {
c := make(chan int)
go func() {
c <- 1
}()
}
程序正常退出了,很简单,并不是我们那个总结不起作用了,还是因为一个让人很囧的原因,main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c信道,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁错误。 那么死锁的解决办法呢? 最简单的,把没取走的数据取走,没放入的数据放入, 因为无缓冲信道不能承载数据,那么就赶紧拿走! 具体来讲,就死锁例子3中的情况,可以这么避免死锁: c, quit := make(chan int), make(chan int)
go func() {
c <- 1
quit <- 0
}()
<- c // 取走c的数据!
<-quit
另一个解决办法是缓冲信道, 即设置c有一个数据的缓冲大小: c := make(chan int, 1)
这样的话,c可以缓存一个数据。也就是说,放入一个数据,c并不会挂起当前线, 再放一个才会挂起当前线直到第一个数据被其他goroutine取走, 也就是只阻塞在容量一定的时候,不达容量不阻塞。 这十分类似我们Python中的队列
观察以下的程序: var ch chan int = make(chan int)
func foo(id int) { //id: 这个routine的标号
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个元素: func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
}
如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。 也就是说,缓冲信道会在满容量的时候加锁。 其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列: 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不等到信道关闭是不会结束读取 |
请发表评论