✍个人博客: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.Mutex
或 sync.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()
方法则可以提高程序的效率,但是需要确保程序的正确性。在实际应用中,应该根据具体情况选择合适的方法。