面试题答案
一键面试Mutex锁
- 优点:
- 实现简单,使用方便,能有效防止数据竞争。对于任何类型的操作(读或写),都只需要加锁和解锁即可,代码逻辑相对清晰。
- 缺点:
- 性能问题,因为Mutex锁是互斥的,同一时间只允许一个协程访问临界区。即使只有读操作,也会被锁限制,导致在高并发读场景下,性能较低。例如,多个协程同时需要读取结构体数据,但由于Mutex锁,它们只能一个一个进行读取,造成不必要的等待。
读写锁(sync.RWMutex)
- 优点:
- 适合读多写少的场景。读操作可以并发执行,只要没有写操作在进行,多个读协程可以同时获取读锁并访问临界区,大大提高了读操作的并发性能。比如在上述场景中,频繁的读操作可以同时进行,提高系统整体性能。
- 缺点:
- 实现相对复杂,需要注意读写锁的嵌套使用等问题。写操作时会独占锁,若写操作频繁,可能导致读操作长时间等待。例如,当一个写操作获取写锁后,所有读操作和其他写操作都要等待该写操作完成释放锁。
锁机制的选择
由于是多个协程频繁读取,偶尔更新操作,即读多写少的场景,读写锁(sync.RWMutex)更合适。这样能充分利用读操作的并发优势,提高系统性能。
优化方案
- 减少锁的粒度:如果结构体中有部分字段是相互独立的,可以将结构体拆分成多个小的结构体,对不同的小结构体使用不同的锁,这样可以提高并发度。例如,对于结构体中相互独立的字段A和字段B,分别使用不同的读写锁。
- 延迟写操作:可以将写操作缓存起来,批量执行,减少写锁的获取次数。比如,使用一个队列来存储写操作,达到一定数量或一定时间间隔后,一次性获取写锁执行所有写操作。
读写锁代码实现
package main
import (
"fmt"
"sync"
)
// 定义复杂结构体
type ComplexStruct struct {
Field1 int
Field2 string
// 其他字段...
sync.RWMutex
}
// 读操作
func (cs *ComplexStruct) Read() {
cs.RLock()
defer cs.RUnlock()
// 读取结构体字段
fmt.Printf("Read: Field1 = %d, Field2 = %s\n", cs.Field1, cs.Field2)
}
// 写操作
func (cs *ComplexStruct) Write(field1 int, field2 string) {
cs.Lock()
defer cs.Unlock()
// 更新结构体字段
cs.Field1 = field1
cs.Field2 = field2
fmt.Printf("Write: Field1 = %d, Field2 = %s\n", cs.Field1, cs.Field2)
}
func main() {
var wg sync.WaitGroup
cs := ComplexStruct{Field1: 1, Field2: "initial"}
// 模拟多个读操作
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cs.Read()
}()
}
// 模拟写操作
wg.Add(1)
go func() {
defer wg.Done()
cs.Write(2, "updated")
}()
wg.Wait()
}
处理读写锁嵌套问题
- 避免不必要的嵌套:尽量设计代码逻辑,避免出现锁的嵌套。例如,将需要嵌套锁的操作拆分,在不同的函数或方法中完成,在调用外层函数前先获取外层锁,调用内层函数时获取内层锁,这样锁的层次更清晰。
- 遵循固定顺序:如果必须嵌套锁,要遵循固定的获取锁顺序。比如先获取读锁,再获取写锁(在允许的情况下),释放锁时按相反顺序。以代码为例,在已经获取读锁的函数中,如果需要更新数据,先释放读锁,获取写锁进行更新,然后再获取读锁继续后续操作。但要注意,这种情况可能会导致死锁,所以要仔细考虑业务逻辑和锁的释放时机。例如,在两个函数中分别获取不同顺序的锁可能会导致死锁,如下:
package main
import (
"fmt"
"sync"
)
var (
mu1 sync.RWMutex
mu2 sync.RWMutex
)
func func1() {
mu1.RLock()
defer mu1.RUnlock()
mu2.Lock()
defer mu2.Unlock()
fmt.Println("func1")
}
func func2() {
mu2.RLock()
defer mu2.RUnlock()
mu1.Lock()
defer mu1.Unlock()
fmt.Println("func2")
}
在这种情况下,两个函数获取锁的顺序不同,可能导致死锁。所以要确保所有涉及锁嵌套的地方,获取锁的顺序是一致的。