var mu sync.Mutex
func produce(ch chan<- int){
for i:=0;i<10;i++{
mu.Lock()
ch<-i
fmt.Println("produce:"+strconv.Itoa(i))
mu.Unlock()
}
}
func consumer(ch <- chan int){
for i:=0;i<10;i++{
mu.Lock()
v:=<-ch
fmt.Println("consumer:"+strconv.Itoa(v))
mu.Unlock()
}
}
func main(){
ch:=make(chan int,5)
go produce(ch)
go consumer(ch)
time.Sleep(10*time.Second)
}
我是 go 的初学者,我今天写了一个问题代码。本意是让资源生成和消费的时候同时打印出该资源信息。 我知道当 consumer 先执行时会导致死锁,但是我不明白的是为什么 produce 先执行时,即使他释放了锁,consumer 也竞争不到锁,produce 会一直占有锁,这里面的竞争规则是什么样的?我在网上没有找到满意的答案
1
silenzio 2020-09-15 09:21:00 +08:00
你可以简单理解为没有规则 完全随机
实际上 如果你要实现的是打印功能 完全不需要锁 这两个 go 程没有修改共享变量 为什么要加锁呢? chan 就可以完成阻塞动作 |
2
Chaox OP @silenzio 谢谢解答。我写锁的目的是为了获得和消耗资源的同时,能够同时打印该资源。我最开始写的是不加锁的,但是我发现有时候他生成资源后,还没来得及打印,消费者已经消费并打印了。打印结果看起来不是很合逻辑。
|
3
zhs227 2020-09-15 09:40:35 +08:00
你已经是走 chan 了,就不用锁了。另外,打印的顺序和多线程中实际执行的时间并不一定强相关
你这样写,我脑补运行了一下,容易导致一边获取了锁以后,chan 又被另一方阻塞,程序完全执行不下去。 |
4
BingoXuan 2020-09-15 09:40:49 +08:00
1. Print 并不保证线程安全,消费者生产者同时打印的结果可能会混起来
2. 或者专门一个协程通过 chan 来获取需要打印的数据 3. 用 sync.WaitGroup 同步不同协程,类似于 thread.join 的效果 |
6
Chaox OP @zhs227 是的,我完全理解你的解答,我的这个代码很有问题,但是我的疑惑是 consumer 永远抢不到 produce 释放的锁。我加锁的目的就是为了打印的顺序和多线程中实际执行的时间相关起来。
|
7
CEBBCAT 2020-09-15 09:56:49 +08:00 via Android
我想问题可能出在对 ch 加锁那里,加入消费者消费过慢,那么有可能 produce 塞满了 ch 后进入下一次循环后,consumer 被锁阻塞了,所以死锁了
关于打印顺序,就是这样的呀,向 ch 填充数据和 fmt 的打印不是原子的,之间可能存在协程切换 我觉得你要么放弃纠结这个,要么换一个更好的办法解决日志问题 go 的协程调度顺序我记不太清除了,但应该是两个 go 指令中的后一个会被先得到执行。但我想没有人会依赖这个 |
8
silenzio 2020-09-15 10:00:02 +08:00
我的意思是你可以假设为安全随机 也就是说 你不能认为你贴出的代码 可以控制它的顺序 你不能假设 produce 永远先执行 你在写代码的时候 必须假设它是完全随机的
复制你的代码用 code runner 执行 5 次 结果如下: 有三次是这个情况: produce 执行 5 次 程序死锁 有一次是这个情况: produce 执行 4 次 consumer 执行 1 次 produce 执行 2 次 程序死锁 有一次是这个情况: consumer 先抢到锁 程序死锁 这个结果受很多因素影响 比如 go 程启动也是需要时间的. 你可以调整代码 先 go consumer(ch) 再 go produce(ch), 会发现 consumer 先抢到锁的几率大大提高 https://mp.weixin.qq.com/s/hIs318h6iJW2O9--QVqh6w |
9
heimeil 2020-09-15 10:03:07 +08:00
var mu sync.Mutex
func produce(ch chan<- int) { for i := 0; i < 10; i++ { mu.Lock() ch <- i fmt.Println("produce:" + strconv.Itoa(i)) mu.Unlock() runtime.Gosched() } } func consumer(ch <-chan int) { for i := 0; i < 10; i++ { runtime.Gosched() mu.Lock() v := <-ch fmt.Println("consumer:" + strconv.Itoa(v)) mu.Unlock() } } func main() { ch := make(chan int, 5) go produce(ch) go consumer(ch) time.Sleep(10 * time.Second) } produce 的 for 循环不停加锁解锁,比其他协程的等待锁释放更容易竞争到锁,用 runtime.Gosched() 让出执行权给其他协程,让别的协程的锁也有拿到锁的可能,不过还是看运气,多跑几次 |
10
Chaox OP @silenzio 谢谢解答。我之前的运行状况是 produce 一直能抢到锁,我误以为其中有什么规则。看了你的解答后我加大了循环次数和管道容量,consumer 终于抢到 produce 释放的锁了。
|
11
zhs227 2020-09-15 10:12:16 +08:00
因为你有 chan,如果没有这个 chan,就可以抢到锁了。但绝对不是平均,一边一次这种。
|
14
lewinlan 2020-09-15 10:26:23 +08:00 via Android
ch 推满了就死锁了。建议仔细复习一下操作系统关于锁的原理。
|
15
walsh 2020-09-15 10:51:24 +08:00
其实有了原子操作,你自己写个锁也没问题,为性能着想再加上等待唤醒
|