面试题答案
一键面试可能用到的场景
假设一个缓存系统,多个协程会频繁读取缓存数据,但偶尔需要更新缓存。当更新缓存时,为了防止读取操作读到不一致的数据,需要使用写锁。在更新完成后,可能需要继续读取更新后的数据,此时可以进行锁降级,即从写锁降级为读锁。例如:
package main
import (
"fmt"
"sync"
)
var (
cache map[string]string
mu sync.RWMutex
)
func updateCache(key, value string) {
mu.Lock()
cache[key] = value
// 这里可以进行锁降级
mu.RLock()
mu.Unlock()
// 后续可以进行读取操作
fmt.Println("Read after update:", cache[key])
mu.RUnlock()
}
面临的风险
- 死锁风险:如果在持有写锁的情况下,先解锁写锁,再尝试获取读锁,而此时有其他协程已经获取了读锁,就会导致死锁。例如:
package main
import (
"fmt"
"sync"
)
var (
cache map[string]string
mu sync.RWMutex
)
func wrongUpdateCache(key, value string) {
mu.Lock()
cache[key] = value
mu.Unlock()
mu.RLock()
fmt.Println("Read after update:", cache[key])
mu.RUnlock()
}
在上述代码中,先解锁写锁再获取读锁,若此时其他协程持有读锁,就会造成死锁。
- 数据竞争风险:如果在锁降级过程中,没有正确控制其他协程的访问,可能会导致数据竞争。比如在解锁写锁后、获取读锁前,有其他协程修改了数据,就会读到不一致的数据。
避免风险的方法
- 正确的锁操作顺序:按照先获取读锁,再解锁写锁的顺序进行锁降级,就像第一个示例代码那样。这样可以保证在持有写锁时获取读锁,避免死锁。
- 同步控制:在锁降级过程中,确保没有其他协程对共享数据进行修改。可以通过在获取写锁期间,禁止其他协程的读写操作,直到锁降级完成。例如可以增加一个标志位,在持有写锁时设置标志位,其他协程检测到标志位则不进行读写操作。