【Golang 面试 - 进阶题】每日 3 题(四)

发布于:2024-08-02 ⋅ 阅读:(79) ⋅ 点赞:(0)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

10. Go 可重入锁如何实现?

概念:

可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法时会自动获取锁,不会因为之前已经获取过还没释放再次加锁导致死锁。

为什么 Go 语言中没有可重入锁?

Mutex 不是可重入的锁。Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件,并且Mutex 重复 Lock 会导致死锁。

如何实现可重入锁?

实现一个可重入锁需要这两点:

  • 记住持有锁的线程

  • 统计重入的次数

package main
import (
    "bytes"
    "fmt"
    "runtime"
    "strconv"
    "sync"
    "sync/atomic"
)
type ReentrantLock struct {
    sync.Mutex
    recursion int32 // 这个goroutine 重入的次数
    owner     int64 // 当前持有锁的goroutine id
}
// Get returns the id of the current goroutine.
func GetGoroutineID() int64 {
    var buf [64]byte
    var s = buf[:runtime.Stack(buf[:], false)]
    s = s[len("goroutine "):]
    s = s[:bytes.IndexByte(s, )]
    gid, _ := strconv.ParseInt(string(s), 10, 64)
    return gid
}
func NewReentrantLock() sync.Locker {
    res := &ReentrantLock{
        Mutex:     sync.Mutex{},
        recursion: 0,
        owner:     0,
    }
    return res
}
// ReentrantMutex 包装一个Mutex,实现可重入
type ReentrantMutex struct {
    sync.Mutex
    owner     int64 // 当前持有锁的goroutine id
    recursion int32 // 这个goroutine 重入的次数
}
func (m *ReentrantMutex) Lock() {
    gid := GetGoroutineID()
    // 如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入
    if atomic.LoadInt64(&m.owner) == gid {
        m.recursion++
        return
    }
    m.Mutex.Lock()
    // 获得锁的goroutine第一次调用,记录下它的goroutine id,调用次数加1
    atomic.StoreInt64(&m.owner, gid)
    m.recursion = 1
}
func (m *ReentrantMutex) Unlock() {
    gid := GetGoroutineID()
    // 非持有锁的goroutine尝试释放锁,错误的使用
    if atomic.LoadInt64(&m.owner) != gid {
        panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
    }
    // 调用次数减1
    m.recursion--
    if m.recursion != 0 { // 如果这个goroutine还没有完全释放,则直接返回
        return
    }
    // 此goroutine最后一次调用,需要释放锁
    atomic.StoreInt64(&m.owner, -1)
    m.Mutex.Unlock()
}
func main() {
    var mutex = &ReentrantMutex{}
    mutex.Lock()
    mutex.Lock()
    fmt.Println(111)
    mutex.Unlock()
    mutex.Unlock()
}

 11. Cond 是什么?

在 Go 语言中,sync.Cond 是一个条件变量的实现,它可以在多个 Goroutine 之间传递信号和数据。条件变量是一种同步机制,用于解决某些 Goroutine 需要等待某个事件或条件发生的问题。

sync.Cond 是基于 sync.Mutexsync.RWMutex 的,它提供了 Wait()Signal()Broadcast() 三个方法。

  • Wait():释放锁并阻塞当前 Goroutine,直到调用 Signal()Broadcast() 并重新获得锁。在阻塞期间,Goroutine 处于等待状态并且不会消耗 CPU 资源。

  • Signal():唤醒一个等待中的 Goroutine。

  • Broadcast():唤醒所有等待中的 Goroutine。

下面是一个简单的示例,展示了如何使用 sync.Cond 实现多个 Goroutine 之间的通信:

package main
import (
    "fmt"
    "sync"
    "time"
)
func main() {
    var mutex sync.Mutex
    cond := sync.NewCond(&mutex)
    ready := false
    for i := 0; i < 5; i++ {
        go func(i int) {
            mutex.Lock()
            defer mutex.Unlock()
            for !ready {
                cond.Wait()
            }
            fmt.Printf("Goroutine %d is now running\n", i)
        }(i)
    }
    time.Sleep(time.Second * 1)
    mutex.Lock()
    ready = true
    cond.Broadcast()
    mutex.Unlock()
    time.Sleep(time.Second * 1)
}

在这个例子中,我们创建了一个 sync.Cond 实例,将它和一个 sync.Mutex 关联起来。然后我们启动了 5 个 Goroutine,在其中每个 Goroutine 上等待一个条件变量。我们在主 Goroutine 中等待一秒钟,然后发送一个 Broadcast 信号,通知所有等待中的 Goroutine 可以开始执行了。在接收到 Broadcast 信号后,所有 Goroutine 将开始执行,并输出一些消息。

12. B r oadcast 和 Signal 区别

在 Go 语言中,sync.Cond 类型提供了 Broadcast()Signal() 两个方法来唤醒等待条件变量的 Goroutine。这两个方法的区别在于:

  • Signal() 方法只会唤醒等待条件变量的一个 Goroutine,具体哪个 Goroutine 会被唤醒是不确定的。如果多个 Goroutine 等待同一个条件变量,那么只会有一个 Goroutine 被唤醒,其他 Goroutine 仍然会继续等待条件变量。

  • Broadcast() 方法会唤醒所有等待条件变量的 Goroutine,使它们都开始运行。如果多个 Goroutine 等待同一个条件变量,那么所有 Goroutine 都会被唤醒。

一般来说,使用 Signal() 方法可以提高程序的效率,因为只需要唤醒一个 Goroutine,其他 Goroutine 仍然会等待条件变量,不会消耗 CPU 资源。但是,如果有多个 Goroutine 都需要同时等待条件变量,那么使用 Broadcast() 方法才能保证它们都能被唤醒,否则可能会出现死锁等问题。

总之,Broadcast() 方法是一种安全可靠的方法,但是可能会导致一些性能问题。而 Signal() 方法则可以提高程序的效率,但是需要确保程序的正确性。在实际应用中,应该根据具体情况选择合适的方法。