互斥锁用对了吗?
Mutex used right?
我对 mutex locken/unlocking 一次又一次感到有点困惑。
我正在使用 RWMutex 并且所有 goroutines 当然会有相同的互斥量。
如此频繁地使用互斥体时,这段代码是否仍然受到竞争保护?
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
r.Mu().RLock()
size := len(r.redisDbs) // A
r.Mu().RUnlock()
if size >= int(dbId) { // B
r.Mu().RLock()
db := r.redisDbs[dbId] // C
r.Mu().RUnlock()
if db != nil { // D
return db
}
}
// E create db...
}
我认为可能发生的示例情况:
- gorountine1和goroutine2都是运行这个函数
- 都在A点所以变量
size
是3
- 两个 goroutines 的条件 B 都是
true
- 两人同时读C
- 变量
db
对于两个goroutines都是nil所以条件C是false
- 现在两个 goroutine 都将转到 E 并创建相同的数据库 2 次,这很糟糕
还是在这种情况下我必须 lock/unlock 一次?
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
r.Mu().Lock()
defer r.Mu().Unlock()
size := len(r.redisDbs)
if size >= int(dbId) {
db := r.redisDbs[dbId]
if db != nil {
return db
}
}
// create db...
}
解决方案
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
getDb := func() *RedisDb { // returns nil if db not exists
if len(r.redisDbs) >= int(dbId) {
db := r.redisDbs[dbId]
if db != nil {
return db
}
}
return nil
}
r.Mu().RLock()
db := getDb()
r.Mu().RUnlock()
if db != nil {
return db
}
// create db
r.Mu().Lock()
defer r.Mu().Unlock()
// check if db does not exists again since
// multiple "mutex readers" can come to this point
db = getDb()
if db != nil {
return db
}
// now really create it
// ...
}
欢迎来到同步世界。您的评估是正确的,第一次实施可能会出现并发问题。对于第二个,那些并发问题被消除了,但它被完全锁定,甚至没有机会进行并发读取访问。您 没有 那样做,您可以使用读锁进行初始检查,然后如果该检查确定需要创建,则建立写锁,然后重新检查, 如果仍然需要则创建,然后解锁。这不是一个不寻常的结构。它的效率较低(由于执行了两次检查)因此您需要权衡取舍,主要取决于执行两次检查的成本以及该功能能够在只读路径。
我对 mutex locken/unlocking 一次又一次感到有点困惑。 我正在使用 RWMutex 并且所有 goroutines 当然会有相同的互斥量。
如此频繁地使用互斥体时,这段代码是否仍然受到竞争保护?
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
r.Mu().RLock()
size := len(r.redisDbs) // A
r.Mu().RUnlock()
if size >= int(dbId) { // B
r.Mu().RLock()
db := r.redisDbs[dbId] // C
r.Mu().RUnlock()
if db != nil { // D
return db
}
}
// E create db...
}
我认为可能发生的示例情况:
- gorountine1和goroutine2都是运行这个函数
- 都在A点所以变量
size
是3 - 两个 goroutines 的条件 B 都是
true
- 两人同时读C
- 变量
db
对于两个goroutines都是nil所以条件C是false
- 现在两个 goroutine 都将转到 E 并创建相同的数据库 2 次,这很糟糕
还是在这种情况下我必须 lock/unlock 一次?
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
r.Mu().Lock()
defer r.Mu().Unlock()
size := len(r.redisDbs)
if size >= int(dbId) {
db := r.redisDbs[dbId]
if db != nil {
return db
}
}
// create db...
}
解决方案
func (r *Redis) RedisDb(dbId DatabaseId) *RedisDb {
getDb := func() *RedisDb { // returns nil if db not exists
if len(r.redisDbs) >= int(dbId) {
db := r.redisDbs[dbId]
if db != nil {
return db
}
}
return nil
}
r.Mu().RLock()
db := getDb()
r.Mu().RUnlock()
if db != nil {
return db
}
// create db
r.Mu().Lock()
defer r.Mu().Unlock()
// check if db does not exists again since
// multiple "mutex readers" can come to this point
db = getDb()
if db != nil {
return db
}
// now really create it
// ...
}
欢迎来到同步世界。您的评估是正确的,第一次实施可能会出现并发问题。对于第二个,那些并发问题被消除了,但它被完全锁定,甚至没有机会进行并发读取访问。您 没有 那样做,您可以使用读锁进行初始检查,然后如果该检查确定需要创建,则建立写锁,然后重新检查, 如果仍然需要则创建,然后解锁。这不是一个不寻常的结构。它的效率较低(由于执行了两次检查)因此您需要权衡取舍,主要取决于执行两次检查的成本以及该功能能够在只读路径。