V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
Chaox
V2EX  ›  问与答

关于 Sync.Mutex 的竞争问题

  •  
  •   Chaox · 2020-09-15 08:53:40 +08:00 · 1526 次点击
    这是一个创建于 1565 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 会一直占有锁,这里面的竞争规则是什么样的?我在网上没有找到满意的答案

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

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

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

    go 的协程调度顺序我记不太清除了,但应该是两个 go 指令中的后一个会被先得到执行。但我想没有人会依赖这个
    silenzio
        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
    heimeil
        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() 让出执行权给其他协程,让别的协程的锁也有拿到锁的可能,不过还是看运气,多跑几次
    Chaox
        10
    Chaox  
    OP
       2020-09-15 10:10:21 +08:00
    @silenzio 谢谢解答。我之前的运行状况是 produce 一直能抢到锁,我误以为其中有什么规则。看了你的解答后我加大了循环次数和管道容量,consumer 终于抢到 produce 释放的锁了。
    zhs227
        11
    zhs227  
       2020-09-15 10:12:16 +08:00
    因为你有 chan,如果没有这个 chan,就可以抢到锁了。但绝对不是平均,一边一次这种。
    silenzio
        12
    silenzio  
       2020-09-15 10:13:04 +08:00
    @Chaox 客气了 共同进步
    Chaox
        13
    Chaox  
    OP
       2020-09-15 10:13:17 +08:00
    @heimeil 谢谢解答,完全理解了。
    lewinlan
        14
    lewinlan  
       2020-09-15 10:26:23 +08:00 via Android
    ch 推满了就死锁了。建议仔细复习一下操作系统关于锁的原理。
    walsh
        15
    walsh  
       2020-09-15 10:51:24 +08:00
    其实有了原子操作,你自己写个锁也没问题,为性能着想再加上等待唤醒
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2790 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 13:39 · PVG 21:39 · LAX 05:39 · JFK 08:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.