协程(goroutine)算是Go的一大新特性,也正是这个大杀器让Go为不少路人驻足欣赏,让信徒们为之欢呼津津乐道。缓存
协程的使用也很简单,在Go中使用关键字“go“后面跟上要执行的函数即表示新启动一个协程中执行功能代码。安全
func main() {
go test()
fmt.Println("it is the main goroutine")
time.Sleep(time.Second * 1)
}
func test() {
fmt.Println("it is a new goroutine")
}
复制代码
能够简单理解为,Go中的协程就是一种更轻、支持更高并发的并发机制。bash
仔细看上面的main函数中有一个休眠一秒的操做,若是去掉该行,则打印结果中就没有“it is a new goroutine”。这是由于新启的协程还没来得及运行,主协程就结束了。并发
因此这里有个问题,咱们怎么样才能让各个协程之间可以知道彼此是否执行完毕呢?异步
显然,咱们能够经过上面的方式,让主协程休眠一秒钟,等等子协程,确保子协程可以执行完。但做为一个新型语言不该该使用这么low的方式啊。连Java这位老前辈都有Future这种异步机制,并且能够经过get方法来阻塞等待任务的执行,确保能够第一时间知晓异步进程的执行状态。函数
因此,Go必需要有过人之处,即另外一个让路人侧目,让信徒为之疯狂的特性——信道(channel)。高并发
信道能够简单认为是协程goroutine之间一个通讯的桥梁,能够在不一样的协程里互通有无穿梭自如,且是线程安全的。ui
信道分为两类spa
无缓冲信道线程
ch := make(chan string)
复制代码
有缓冲信道
ch := make(chan string, 2)
复制代码
一、从声明方式来看,有缓冲带了容量,即后面的数字,这里的2表示信道能够存放两个stirng类型的变量
二、无缓冲信道自己不存储信息,它只负责转手,有人传给它,它就必需要传给别人,若是只有进或者只有出的操做,都会形成阻塞。有缓冲的能够存储指定容量个变量,可是超过这个容量再取值也会阻塞。
无缓冲信道
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
fmt.Println(<-ch)
}
复制代码
在主协程中新启一个协程且是匿名函数,在子协程中向通道发送“send”,经过打印结果,咱们知道在主线程使用<-ch接收到了传给ch的值。
<-ch是一种简写方式,也可使用str := <-ch方式接收信道值。
上面是在子协程中向信道传值,并在主协程取值,也能够反过来,一样能够正常打印信道的值。
func main() {
ch := make(chan string)
go func() {
fmt.Println(<-ch)
}()
ch <- "send"
}
复制代码
有缓冲信道
func main() {
ch := make(chan string, 2)
ch <- "first"
ch <- "second"
fmt.Println(<-ch)
fmt.Println(<-ch)
}
复制代码
执行结果为
first
second
复制代码
信道自己结构是一个先进先出的队列,因此这里输出的顺序如结果所示。
从代码来看这里也不须要从新启动一个goroutine,也不会发生死锁(后面会讲缘由)。
信道是能够关闭的。对于无缓冲和有缓冲信道关闭的语法都是同样的。
close(channelName)
复制代码
注意信道关闭了,就不能往信道传值了,不然会报错。
func main() {
ch := make(chan string, 2)
ch <- "first"
ch <- "second"
close(ch)
ch <- "third"
}
复制代码
报错信息
panic: send on closed channel
复制代码
有缓冲信道是有容量的,因此是能够遍历的,而且支持使用咱们熟悉的range遍历。
func main() {
chs := make(chan string, 2)
chs <- "first"
chs <- "second"
for ch := range chs {
fmt.Println(ch)
}
}
复制代码
输出结果为
first
second
fatal error: all goroutines are asleep - deadlock!
复制代码
没错,若是取完了信道存储的信息再去取信息,也会死锁(后面会讲)
有了前面的介绍,咱们大概知道了信道是什么,如何使用信道。
下面就来讲说信道死锁的场景和为何会死锁(有些是本身的理解,可能有误差,若有问题请指正)。
func main() {
ch := make(chan string)
ch <- "channelValue"
}
复制代码
func main() {
ch := make(chan string)
<-ch
}
复制代码
这两种状况,即不管是向无缓冲信道传值仍是取值,都会发生死锁。
缘由分析
如上场景是在只有一个goroutine即主goroutine的,且使用的是无缓冲信道的状况下。
前面提过,无缓冲信道不存储值,不管是传值仍是取值都会阻塞。这里只有一个主协程的状况下,第一段代码是阻塞在传值,第二段代码是阻塞在取值。由于一直卡住主协程,系统一直在等待,因此系统判断为死锁,最终报deadlock错误并结束程序。
延伸
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
}
复制代码
这种状况不会发生死锁。
有人说那是由于主协程发车太快,子协程还没看到,车就开走了,因此没来得及抱怨(deadlock)就结束了。
其实不是这样的,下面举个反例
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
time.Sleep(time.Second * 3)
}
复制代码
此次主协程等你了三秒,三秒你总该完事了吧?!
可是从执行结果来看,并无子协程由于一直阻塞就形成报死锁错误。
这是由于虽然子协程一直阻塞在传值语句,但这也只是子协程的事。外面的主协程仍是该干吗干吗,等你三秒以后就发车走人了。由于主协程都结束了,因此子协程也只好结束(毕竟没搭上车只能回家了,光杵在哪也于事无补)
紧接着上面死锁现场1的延伸场景,咱们提到延伸场景没有死锁是由于主协程发车走了,因此子协程也只能回家。也就是二者没有耦合的关系。
若是二者经过信道创建了联系还会死锁吗?
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch1
}
复制代码
执行结果为
fatal error: all goroutines are asleep - deadlock!
复制代码
没错,这样就会发生死锁。
缘由分析
上面的代码不能保证是主线程的<-ch1先执行仍是子协程的代码先执行。
若是主协程先执行到<-ch1,显然会阻塞等待有其余协程往ch1传值。终于等到子协程运行了,结果子协程运行ch2 <- "ch2 value"就阻塞了,由于是无缓冲,因此必须有下家接收值才行,可是等了半天也没有人来传值。
因此这时候就出现了主协程等子协程的ch1,子协程在等ch2的接收者,ch1<-“ch1 value”语句迟迟拿不到执行权,因而你们都在相互等待,系统看不下去了,断定死锁,程序结束。
相反执行顺序也是同样。
延伸
有人会说那我改为这样能避免死锁吗
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch1
<- ch2
}
复制代码
不行,执行结果依然是死锁。由于这样的顺序仍是改变不了主协程和子协程相互等待的状况,即死锁的触发条件。
改成下面这样就能够正常结束
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch2
<- ch1
}
复制代码
借此,经过下面的例子再验证上面死锁现场1是由于主协程没受到死锁的影响因此不会报死锁错误的问题
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
go func() {
<- ch1
<- ch2
}()
time.Sleep(time.Second * 2)
}
复制代码
咱们刚刚看到若是
<- ch1
<- ch2
复制代码
放到主协程,则会由于相互等待发生死锁。可是这个例子里,将一样的代码放到一个新启的协程中,尽管两个子协程存在阻塞死锁的状况,可是不会影响主协程,因此程序执行不会报死锁错误。
func main() {
chs := make(chan string, 2)
chs <- "first"
chs <- "second"
for ch := range chs {
fmt.Println(ch)
}
}
复制代码
输出结果为
first
second
fatal error: all goroutines are asleep - deadlock!
复制代码
缘由分析
为何会在输出完chs信道全部缓存值后会死锁呢?
其实也很简单,虽然这里的chs是带有缓冲的信道,可是容量只有两个,当两个输出完以后,能够简单的将此时的信道等价于无缓冲的信道。
显然对于无缓冲的信道只是单纯的读取元素是会形成阻塞的,并且是在主协程,因此和死锁现场1等价,故而会死锁。
一、信道是协程之间沟通的桥梁
二、信道分为无缓冲信道和有缓冲信道
三、信道使用时要注意是否构成死锁以及各类死锁产生的缘由