面试题答案
一键面试读锁和写锁在加锁、解锁操作以及并发访问控制方面的区别
- 加锁操作
- 读锁(RLock):多个读操作可以同时持有读锁,只要没有写锁被持有。当调用
RLock
方法时,如果此时没有写锁被持有,读锁立即获取成功,多个读操作可以并行执行。 - 写锁(Lock):写锁是排他性的,当调用
Lock
方法时,如果此时有读锁或其他写锁被持有,写锁会阻塞,直到所有读锁和写锁都被释放,才能获取写锁成功。
- 读锁(RLock):多个读操作可以同时持有读锁,只要没有写锁被持有。当调用
- 解锁操作
- 读锁(RUnlock):读锁通过
RUnlock
方法解锁。每一个RLock
调用都应该对应一个RUnlock
调用,以释放读锁资源。如果读锁解锁次数多于加锁次数,会导致运行时错误。 - 写锁(Unlock):写锁通过
Unlock
方法解锁。同样,每一个Lock
调用都应该对应一个Unlock
调用,否则也会导致运行时错误。
- 读锁(RUnlock):读锁通过
- 并发访问控制
- 读锁:适用于多个协程同时读取共享资源的场景,允许多个读操作并发进行,提高了读操作的并发性能。但是读锁不能与写锁同时存在,因为写操作需要对共享资源进行修改,必须保证数据一致性。
- 写锁:用于对共享资源进行写操作的场景,写锁会阻止其他读锁和写锁的获取,确保在写操作期间,没有其他协程能够访问共享资源,从而保证数据的一致性。
实际应用场景中正确使用读锁和写锁以提高程序性能的示例
假设我们有一个应用场景,需要在内存中维护一个用户信息的缓存。多个协程可能会频繁读取用户信息,但偶尔也会有协程更新用户信息。
package main
import (
"fmt"
"sync"
)
type UserCache struct {
data map[string]string
mu sync.RWMutex
}
// GetUser 获取用户信息
func (uc *UserCache) GetUser(key string) string {
uc.mu.RLock()
defer uc.mu.RUnlock()
return uc.data[key]
}
// SetUser 设置用户信息
func (uc *UserCache) SetUser(key, value string) {
uc.mu.Lock()
defer uc.mu.Unlock()
if uc.data == nil {
uc.data = make(map[string]string)
}
uc.data[key] = value
}
func main() {
var wg sync.WaitGroup
uc := UserCache{}
// 模拟多个读操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("user%d", id)
value := uc.GetUser(key)
fmt.Printf("Reader %d got value: %s\n", id, value)
}(i)
}
// 模拟写操作
wg.Add(1)
go func() {
defer wg.Done()
uc.SetUser("user1", "John Doe")
fmt.Println("Writer updated user1")
}()
wg.Wait()
}
在上述代码中:
GetUser
方法使用读锁(RLock
),因为它只是读取数据,允许多个协程同时调用,提高了读取的并发性能。SetUser
方法使用写锁(Lock
),因为它会修改共享资源(data
map),为了保证数据一致性,需要独占访问,防止其他读或写操作干扰。这样通过合理使用读锁和写锁,既保证了数据一致性,又提高了程序在读写并发场景下的性能。