关于 Sync.Mutex 的竞争问题

2020-09-15 08:53:40 +08:00
 Chaox
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 会一直占有锁,这里面的竞争规则是什么样的?我在网上没有找到满意的答案

1518 次点击
所在节点    问与答
15 条回复
silenzio
2020-09-15 09:21:00 +08:00
你可以简单理解为没有规则 完全随机
实际上 如果你要实现的是打印功能 完全不需要锁
这两个 go 程没有修改共享变量 为什么要加锁呢? chan 就可以完成阻塞动作
Chaox
2020-09-15 09:35:07 +08:00
@silenzio 谢谢解答。我写锁的目的是为了获得和消耗资源的同时,能够同时打印该资源。我最开始写的是不加锁的,但是我发现有时候他生成资源后,还没来得及打印,消费者已经消费并打印了。打印结果看起来不是很合逻辑。
zhs227
2020-09-15 09:40:35 +08:00
你已经是走 chan 了,就不用锁了。另外,打印的顺序和多线程中实际执行的时间并不一定强相关
你这样写,我脑补运行了一下,容易导致一边获取了锁以后,chan 又被另一方阻塞,程序完全执行不下去。
BingoXuan
2020-09-15 09:40:49 +08:00
1. Print 并不保证线程安全,消费者生产者同时打印的结果可能会混起来
2. 或者专门一个协程通过 chan 来获取需要打印的数据
3. 用 sync.WaitGroup 同步不同协程,类似于 thread.join 的效果
Chaox
2020-09-15 09:41:33 +08:00
@silenzio 而且好像获取规则也不是完全随机,因为我发现 produce 先执行,produce 能够一直抢到锁
Chaox
2020-09-15 09:46:46 +08:00
@zhs227 是的,我完全理解你的解答,我的这个代码很有问题,但是我的疑惑是 consumer 永远抢不到 produce 释放的锁。我加锁的目的就是为了打印的顺序和多线程中实际执行的时间相关起来。
CEBBCAT
2020-09-15 09:56:49 +08:00
我想问题可能出在对 ch 加锁那里,加入消费者消费过慢,那么有可能 produce 塞满了 ch 后进入下一次循环后,consumer 被锁阻塞了,所以死锁了

关于打印顺序,就是这样的呀,向 ch 填充数据和 fmt 的打印不是原子的,之间可能存在协程切换

我觉得你要么放弃纠结这个,要么换一个更好的办法解决日志问题

go 的协程调度顺序我记不太清除了,但应该是两个 go 指令中的后一个会被先得到执行。但我想没有人会依赖这个
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
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() 让出执行权给其他协程,让别的协程的锁也有拿到锁的可能,不过还是看运气,多跑几次
Chaox
2020-09-15 10:10:21 +08:00
@silenzio 谢谢解答。我之前的运行状况是 produce 一直能抢到锁,我误以为其中有什么规则。看了你的解答后我加大了循环次数和管道容量,consumer 终于抢到 produce 释放的锁了。
zhs227
2020-09-15 10:12:16 +08:00
因为你有 chan,如果没有这个 chan,就可以抢到锁了。但绝对不是平均,一边一次这种。
silenzio
2020-09-15 10:13:04 +08:00
@Chaox 客气了 共同进步
Chaox
2020-09-15 10:13:17 +08:00
@heimeil 谢谢解答,完全理解了。
lewinlan
2020-09-15 10:26:23 +08:00
ch 推满了就死锁了。建议仔细复习一下操作系统关于锁的原理。
walsh
2020-09-15 10:51:24 +08:00
其实有了原子操作,你自己写个锁也没问题,为性能着想再加上等待唤醒

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/707034

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX