目录
一、通道阻塞/死锁示例
死锁:所有的线程或进程都在等待资源的释放
1)死锁示例一
func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 信道ch被锁
}
//执行这个程序你会看到Go报这样的错误:
fatal error: all goroutines are asleep - deadlock!
如上的程序中, 只有一个goroutine, 所以当你向里面加数据或者存数据的话,都会锁死信道, 并且阻塞当前 goroutine
2)死锁示例二
var ch1 chan int = make(chan int)
var ch2 chan int = 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)死锁总结
默认的,信道的存消息和取消息都是阻塞的,也就是说,无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。
非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。所有不成对向的信道存取数据几乎都会发生死锁,但是也有特殊的,如下:
func main() {
c := make(chan int)
go func() {
c <- 1
}()
}
程序正常退出了,很简单,并不是我们那个总结不起作用了,main又没等待其它goroutine,自己先跑完了, 所以没有数据流入c信道,一共执行了一个goroutine, 并且没有发生阻塞,所以没有死锁错误。所说可以让信道来阻塞主线( <- c)
二、死锁解决办法
1、方法一:先消费channel
package main
import (
"fmt"
)
func f1(in chan int) {
fmt.Println(<-in)
}
func main() {
out := make(chan int)
//调整下位置,先消费
go f1(out)
out <- 2
}
2、方法二:用缓冲通道
加buffer变成有缓冲信道,缓冲信道它是有容量的,存入一个数据的话 , 可以先放在信道里,不必阻塞当前线而等待该数据取走。当缓冲信道达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了,「你们必须把 数据拿走,才可以流入数据」。
缓冲信道ch可以无缓冲的流入3个元素
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
}
如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。也就是说,缓冲信道会在满容量的时候加锁。
三、信道数据的进出顺序
1、无缓冲信道
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)
}
}
无缓冲信道的数据是先到先出,但是 无缓冲信道并不存储数据,只负责数据的流通
2、缓冲信道
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
}
缓冲信道也是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列
四、等待多gorountine的方案
那好,我们回到最初的一个问题,使用信道堵塞主线,等待开出去的所有goroutine跑完。
这是一个模型,开出很多小goroutine, 它们各自跑各自的,最后跑完了向主线报告。
我们讨论如下2个版本的方案:
- 只使用单个无缓冲信道阻塞主线
- 使用容量为goroutines数量的缓冲信道
对于方案1, 示例的代码大概会是这个样子:
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
其实区别仅仅在于一个是缓冲的,一个是非缓冲的
对于这个场景而言,两者都能完成任务, 都是可以的。
- 无缓冲的信道是一批数据一个一个的「流进流出」
- 缓冲信道则是一个一个存储,然后一起流出去