1. 先来一段内含高能bug的golang代码
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var cond sync.Cond
func producer(in chan<- int) {
for {
cond.L.Lock()
for len(in) >= 3 {
cond.Wait()
}
num := rand.Intn(100)
fmt.Println("生产:", num)
in <- num
cond.Signal()
cond.L.Unlock()
}
}
func customer(out <-chan int) {
for {
cond.L.Lock()
for len(out) == 0 {
cond.Wait()
}
num := <-out
fmt.Println("消费:", num)
cond.Signal()
cond.L.Unlock()
}
}
func main() {
rand.Seed(time.Now().UnixNano())
c := make(chan int, 50)
cond.L = new(sync.Mutex)
for i := 1; i <= 5; i++ {
go producer(c)
}
for i := 1; i <= 5; i++ {
go customer(c)
}
select {}
}
2. 这段代码实现了生产者和消费者模型,但是分分钟死锁。。。。
3. 死锁产生场景分析:
考虑初始状态,当前通道内有0个数据,所有customer先执行,然后全部阻塞在wait(处于asleep状态),然后跑在5个cpu线程上的5个producer3个正在生产数据,另外两个在阻塞(处于asleep状态),当3个producer生产完数据后,每次调用signal函数却都唤醒了1个处于asleep状态的producer,处于asleep状态的producer被唤醒后发现条件仍然不满足,继续阻塞在wait函数(处于asleep状态),这时,生产完3个数据后,先前的3个producer也进入了asleep状态,而主线程仍然阻塞在select语句,至此,所有goroutine都被阻塞了,死锁产生了
4. 产生原因分析:
1. signal函数只是无脑的唤醒一个goroutine,它并不会区分是customer还是producer
2.在多核场景下,会产生上述的场景
3. 在单核场景下(尤其是在生产者远远多于消费者的场景时,比如5:50),由于signal是异步的,会导致类似上述场景
5. 程序bug修复:
方案1. 把signal函数换成broadcast函数,但是这样会带来惊群,导致对cpu的竞争,降低效率,于cond设计的初衷不服
方案2. 在wait函数前边加上signal函数,这样就可以在唤醒错误的goroutine的时候在重新发送一个“补偿”信号,但是这样也会导致不必要的cpu竞争,但是没有方案1那么激烈
6. 总结:
归根结底,1. signal函数唤醒的goroutine具有不确定性;2. signal函数是异步的