• golang 条件变量详解


    1:为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?

    2:为什么要用for语句来包裹调用其Wait方法的表达式,用if语句不行吗?

    这些问题我在面试的时候也经常问。你需要对这个Wait方法的内部机制有所了解才能回答上来。

    条件变量的Wait方法主要做了四件事。

    1. 把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
    2. 解锁当前的条件变量基于的那个互斥锁。
    3. 让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
    4. 如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。

    问题一解答:因为条件变量的Wait方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的 panic。为什么条件变量的Wait方法要这么做呢?你可以想象一下,如果Wait方法在互斥锁已经锁定的情况下,阻塞了当前的 goroutine,那么又由谁来解锁呢?别的 goroutine 吗?先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的 goroutine 可以来解锁,那万一解锁重复了怎么办?由此引发的 panic 可是无法恢复的。如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即使当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait方法,并再次被阻塞。所以说,如果条件变量的Wait方法不先解锁互斥锁的话,那么就只会造成两种后果:不是当前的程序因 panic 而崩溃,就是相关的 goroutine 全面阻塞。

    问题二解答:这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。

    1. 有多个 goroutine 在等待共享资源的同一种状态。比如,它们都在等mailbox变量的值不为0的时候再把它的值变为0,这就相当于有多个人在等着我向信箱里放置情报。虽然等待的 goroutine 有多个,但每次成功的 goroutine 却只可能有一个。别忘了,条件变量的Wait方法会在当前的 goroutine 醒来后先重新锁定那个互斥锁。在成功的 goroutine 最终解锁互斥锁之后,其他的 goroutine 会先后进入临界区,但它们会发现共享资源的状态依然不是它们想要的。这个时候,for循环就很有必要了。
    2. 共享资源可能有的状态不是两个,而是更多。比如,mailbox变量的可能值不只有0和1,还有2、3、4。这种情况下,由于状态在每次改变后的结果只可能有一个,所以,在设计合理的前提下,单一的结果一定不可能满足所有 goroutine 的条件。那些未被满足的 goroutine 显然还需要继续等待和检查。
    3. 有一种可能,共享资源的状态只有两个,并且每种状态都只有一个 goroutine 在关注,就像我们在主问题当中实现的那个例子那样。不过,即使是这样,使用for语句仍然是有必要的。原因是,在一些多 CPU 核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的,即使是操作系统(比如 Linux)本身提供的条件变量也会如此

    来看下signal()代码示列

    type User struct { //模拟用户
        mail *Mail
    }
    
    type Mail struct { //模拟邮箱
        mail bool
        sendCond *sync.Cond
        recvCond *sync.Cond
        lock sync.RWMutex
        sendName string
        recvName string
    }
    
    func  NewMail() *Mail {
        m := Mail{}
        m.sendCond = sync.NewCond(&m.lock)
        m.recvCond = sync.NewCond(m.lock.RLocker())
        return &m
    }
    
    func (m *Mail) SendMail() {
        m.lock.Lock()
        time.Sleep(time.Minute)
        if m.mail{
            m.sendCond.Wait()
            fmt.Println("已经有邮件了等待接收")
        }
        m.mail = true
        fmt.Println("发送了一封邮件给:",m.recvName)
        m.lock.Unlock()
        m.recvCond.Signal() //向接收者发送消息
    }
    
    func (m *Mail) RecvMail() {
        m.lock.RLock()
        if !m.mail{
            fmt.Println("没有邮件,等待邮箱发送邮件")
            m.recvCond.Wait()
    
        }
        m.mail  = false
        fmt.Printf("%s收到一封%s发送的邮件
    ",m.recvName,m.sendName)
        m.lock.RUnlock()
        m.sendCond.Signal()
    }
    
    
    func main()  {
        mail := NewMail()
        lock := make(chan struct{},2)
        use1 := User{}
        use1.mail = mail
        use1.mail.sendName = "jacky"
        use2 := User{}
        use2.mail = mail
        use2.mail.recvName = "rose"
    
        //send mail
        go func() {
            defer func() {
                lock <- struct{}{}
            }()
    
            for{
                use1.mail.SendMail()
                time.Sleep(time.Second)
            }
        }()
    
        go func() {
            defer func() {
                lock <- struct{}{}
            }()
            for{
                use2.mail.RecvMail()
                time.Sleep(time.Second)
            }
        }()
    
        <-lock
        <-lock
    }

    我们再看下条件变量的Broadcast()通知方法示列

    func main() {
        // mailbox 代表信箱。
        // 0代表信箱是空的,1代表信箱是满的。
        var mailbox uint8
        // lock 代表信箱上的锁。
        var lock sync.Mutex
        // sendCond 代表专用于发信的条件变量。
        sendCond := sync.NewCond(&lock)
        // recvCond 代表专用于收信的条件变量。
        recvCond := sync.NewCond(&lock)
    
        // send 代表用于发信的函数。
        send := func(id, index int) {
            lock.Lock()
            for mailbox == 1 {
                sendCond.Wait()
            }
            log.Printf("发送邮件用户%d:准备发送邮件...",index)
            mailbox = 1
            log.Printf("发送邮件用户%d:发送了一份邮件...",index)
            lock.Unlock()
            recvCond.Broadcast()
        }
    
        // recv 代表用于收信的函数。
        recv := func(id, index int) {
            lock.Lock()
            for mailbox == 0 {
                recvCond.Wait()
            }
            log.Printf("用户%d收到邮件:发现有份邮件待接收...",index)
            mailbox = 0
            log.Printf("用户%d收到邮件:收到一份邮件...",index)
            lock.Unlock()
            sendCond.Signal() // 确定只会有一个发信的goroutine。
        }
    
        // sign 用于传递演示完成的信号。
        sign := make(chan struct{}, 3)
        max := 6
        go func(id, max int) { // 用于发信。
            defer func() {
                sign <- struct{}{}
            }()
            for i := 1; i <= max; i++ {
                time.Sleep(time.Millisecond * 500)
                send(id, i)
            }
        }(0, max)
        go func(id, max int) { // 用于收信。
            defer func() {
                sign <- struct{}{}
            }()
            for j := 1; j <= max; j++ {
                time.Sleep(time.Millisecond * 200)
                recv(id, j)
            }
        }(1, max/2)
        go func(id, max int) { // 用于收信。
            defer func() {
                sign <- struct{}{}
            }()
            for k := 1; k <= max; k++ {
                time.Sleep(time.Millisecond * 200)
                recv(id, k)
            }
        }(2, max/2)
    
        <-sign
        <-sign
        <-sign
    }
  • 相关阅读:
    剑指offer——斐波那契数列
    剑指offer——用两个栈实现队列
    剑指offer——二维数组中的查找
    LeetCode第九题—— Palindrome Number(判断回文数)
    java 面试题汇总
    idea设置方法注释
    解决java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
    Timer和TimerTask详解
    java8函数式接口(Functional Interface)
    Python执行选择性粘贴
  • 原文地址:https://www.cnblogs.com/jackey2015/p/11748484.html
Copyright © 2020-2023  润新知