Golang RWMutex读写锁的用法(附带实例)
互斥锁是控制多个协程访问同一个资源的常用手段,虽然它保证了数据安全,但同时也降低了性能。
互斥锁用于确保同一时刻只有一个协程访问共享资源,在写少读多的情况下,即使一段时间内没有写操作,大量的并发读操作也会在互斥锁的保护下以串行的方式进行。
而实际上,并不是所有的场景下都需要使用互斥锁来公平地对待读、写。来看个类比,我们去商场买衣服,衣服都是可以随便看的,这可以看作是“读锁”,只有看上自己喜欢的衣服我们才会去试穿,这可以看作是“写锁”。在这种场景下,“读”与“写”是区别对待的。如果只是看衣服,不管有多少人,都可以让他们看,不用排队。只有在选择试衣服时,才会把衣服取下来给他试穿,这时其他人才看不了,直到试穿完毕,归还衣服为止。
许多编程语言都实现了类似的并发锁,即读写锁。读写锁通常都是基于互斥锁、条件变量和信号量等并发技术实现的。
如下表所示,读写锁的设计策略可以分成三类:
读写锁基于互斥锁实现,可以区别对待读和写操作:
读写锁提供的方法如下:
这里模拟的是典型的读多写少的场景,主协程每秒调用一次写操作,100 个子协程每毫秒执行一次读操作。可以看到,通过使用读写锁 RWMutex,大大提高了计数器的性能。
在上述代码中,reader 可以并发进行读操作。如果在这个场景下使用互斥锁,性能就会低很多,因为每个 reader 进行读操作时都会加锁,其他没有获取到锁的 reader 只能排队等待。
互斥锁用于确保同一时刻只有一个协程访问共享资源,在写少读多的情况下,即使一段时间内没有写操作,大量的并发读操作也会在互斥锁的保护下以串行的方式进行。
而实际上,并不是所有的场景下都需要使用互斥锁来公平地对待读、写。来看个类比,我们去商场买衣服,衣服都是可以随便看的,这可以看作是“读锁”,只有看上自己喜欢的衣服我们才会去试穿,这可以看作是“写锁”。在这种场景下,“读”与“写”是区别对待的。如果只是看衣服,不管有多少人,都可以让他们看,不用排队。只有在选择试衣服时,才会把衣服取下来给他试穿,这时其他人才看不了,直到试穿完毕,归还衣服为止。
读写锁的设计策略
在设计锁的读、写策略时,借鉴了现实生活中的处理方式。当一个协程访问共享资源时,会先对其进行判断,如果它是写操作,那么就让它独占这把锁;如果不是,就让所有读操作的协程共享这把锁。这样一来,串行变成了并行,从而提高了性能。许多编程语言都实现了类似的并发锁,即读写锁。读写锁通常都是基于互斥锁、条件变量和信号量等并发技术实现的。
如下表所示,读写锁的设计策略可以分成三类:
设计方案 | 设计说明 |
---|---|
读优先级更高 | 这种设计可以提供高并发,但在竞争激烈的情况下可能会导致写饥饿(write-starvation)。这是因为如果有大量的读,将导致只有在所有读取都释放了锁之后写才能获取锁。 |
写优先级更高 | 这种设计主要避免了写饥饿问题。如果同一时间有一个 reader 和 writer 在等待获取锁,那么会优先给 writer,且会阻止 reader 获取锁。当然,如果 reader 已经获得了锁,那么 writer 也会等待现有 reader 释放锁,然后再获取它。 |
未定义优先级 | 这种设计相对简单,不区分 reader 和 writer 的优先级。在某些情况下,这种不指定优先级的设计更有效,因为读和写都有同样的优先权,这也解决了饥饿问题。 |
“写优先级更高”的RWMutex
Go 标准库中的读写锁 sync.RWMutex(简称为 RWMutex)是基于写优先级更高的方案设计的。读写锁基于互斥锁实现,可以区别对待读和写操作:
- 同一时刻只能有一个协程获得写锁,但可以有任意数量的协程获得读锁;
- 读和写互斥,两种操作不能同时进行;
- 多个读操作可以同时进行;
- 当协程持有写锁时,会阻塞所有读或写的协程;
- 当协程持有读锁时,不会阻塞读的协程,只会阻塞写的协程;
- 读写锁通常用于有大量读少量写的场景。
读写锁提供的方法如下:
func (rw *RWMutex) Lock() func (rw *RWMutex) Unlock() func (rw *RWMutex) RLock() func (rw *RWMutex) RUnlock()
Go语言RWMutex的使用示例
这里基于读写锁 RWMutex 实现了一个线程安全的计数器,示例代码如下:type Counter struct { sync.RWMutex count uint64 } // 使用读锁保护 func (c *Counter) Query() uint64 { c.RLock() defer c.RUnlock() return c.count } // 使用写锁保护 func (c *Counter) Increase() { c.Lock() c.count++ c.Unlock() } func main() { var counter Counter for i := 0; i < 100; i++ { go func() { for { counter.Query() time.Sleep(time.Millisecond) } }() } for { // 1个writer counter.Increase() // 计数器写操作 time.Sleep(time.Second) } }通过上述代码可以看到,方法 Increase() 是写操作,它使用方法 Lock()/Unlock() 进行加锁和释放锁操作。方法 Query() 是读操作,它使用方法 RLock()/RUnlock() 进行加锁和释放锁操作。
这里模拟的是典型的读多写少的场景,主协程每秒调用一次写操作,100 个子协程每毫秒执行一次读操作。可以看到,通过使用读写锁 RWMutex,大大提高了计数器的性能。
在上述代码中,reader 可以并发进行读操作。如果在这个场景下使用互斥锁,性能就会低很多,因为每个 reader 进行读操作时都会加锁,其他没有获取到锁的 reader 只能排队等待。