赞
踩
读写锁是一种并发控制机制,它允许多个线程同时对共享资源进行读访问,但在进行写操作时需要互斥访问,以确保数据的一致性和完整性。读写锁的主要功能是提高系统在读多写少场景下的并发处理能力,从而提升整体性能。
读写锁在实际应用中扮演着重要的角色,可以有效地降低系统的并发访问冲突,提高系统的并发处理能力,同时也能够避免写操作对读操作的阻塞,从而减少了线程的等待时间,提升了系统的整体响应速度。
读写锁的特点主要包括以下几点:
应用场景包括但不限于:
这些场景下,读写锁能够有效地提高系统的并发处理能力,降低线程的竞争压力,从而提升系统的整体性能。
以上是对读写锁的概述,通过对读写锁的功能及作用、特点及应用场景的描述,我们可以深入理解读写锁在并发编程中的重要作用,以及在实际应用中的价值所在。
在 上一章节 中,我们简单介绍了读写锁的概念和应用场景。现在,让我们深入 Golang 的内部,探索读写锁的基本实现原理,并了解其使用规范、特性和局限性。
Golang 标准库 sync
包提供了 RWMutex
类型来实现读写锁。RWMutex
的操作可以概括为以下几种:
操作 | 描述 |
---|---|
Lock() | 获取写锁,阻塞直到获取成功 |
Unlock() | 释放写锁 |
RLock() | 获取读锁,允许多个 goroutine 同时获取 |
RUnlock() | 释放读锁 |
下面是一个简单的示例,展示了如何使用 RWMutex
保护共享资源:
package main import ( "fmt" "sync" "time" ) type Counter struct { mu sync.RWMutex count int } func (c *Counter) Inc() { c.mu.Lock() // 获取写锁 defer c.mu.Unlock() // 函数结束时释放写锁 c.count++ } func (c *Counter) Get() int { c.mu.RLock() // 获取读锁 defer c.mu.RUnlock() // 函数结束时释放读锁 return c.count } func main() { counter := &Counter{} // 多个 goroutine 并发读写 go func() { for i := 0; i < 10; i++ { counter.Inc() time.Sleep(time.Millisecond) } }() go func() { for i := 0; i < 10; i++ { fmt.Println("Count:", counter.Get()) time.Sleep(time.Millisecond * 2) } }() time.Sleep(time.Second) }
在上述示例中,Counter
结构体使用 RWMutex
保护 count
字段。Inc()
方法修改 count
时获取写锁,确保同一时刻只有一个 goroutine 可以修改数据。Get()
方法读取 count
时获取读锁,允许多个 goroutine 同时读取数据。
并发编程中经常涉及到读写操作。读操作不会影响数据的并发性,但写操作需要保证数据的一致性。为此,Go 语言提供了 RWMutex 读写锁用于保护共享资源,实现高效的并发性。
读写锁 RWMutex 是通过嵌套 Mutex 与 Cond 结构实现的,相关定义如下:
type RWMutex struct { w Mutex // 互斥锁 writerSem uint32 // Writer notification semaphore. readerSem uint32 // Reader notification semaphore. readerCount int32 // Number of readers. readerWait int32 // Waiting readers. } type Mutex struct { state int32 sema uint32 } type Cond struct { noCopy noCopy L *Locker notify notifyList }
下面结合表格描述 RWMutex 结构体包含的主要字段及功能。
字段名 | 类型 | 功能 |
---|---|---|
w | Mutex | 写操作互斥锁,用于控制多个写操作互斥。 |
writerSem | uint32 | 写操作信号量,用于告诉等待写锁的 goroutine 其它的 goroutine 正在读或写该锁。writerSem 和 readerSem 都是信号量,它们的值表示有多少个 goroutine 正在一等待写锁和读锁的释放。 |
readerSem | uint32 | 读操作信号量,用于告诉等待读锁的 goroutine 其它的 goroutine 正在写该锁。 |
readerCount | int32 | 当前读锁的持有数。 |
readerWait | int32 | 等待读锁的 goroutine 数量。 |
这些字段及方法相互协作,实现了 RWMutex 的读写锁功能。
RWMutex 包含了以下几个方法,分别对应加读锁、解读锁、加写锁、解写锁以及重锁。下面将分别介绍这几个方法的实现。
RLoser/Locker 接口是一个 RWMutex 所有方法的接口,在部分实现 RWMutex 方法中需要访问该接口。
type RLocker interface {
RLoser
Lock()
Unlock()
}
type RLoscker interface {
RLock()
RUnlock()
}
其中 RLocker 继承了 RLoscker 接口,并添加了 Lock 和 Unlock 方法。RLocker 接口表示对象既支持读操作又支持写操作。
RLock 方法实现加读锁操作,RUnlock 方法实现解读锁操作。这两个方法的实现非常简单,具体可以看下面的代码。
// RLock 加读锁操作 func (rw *RWMutex) RLock() { runtime_Semacquire(&rw.readerSem) // Increment reader count. n := atomic.AddInt32(&rw.readerCount, 1) // if there is a writer, or a wait for a writer waiting, wait. if rw.writerSem != 0 || n < 0 { // Slow path: semaphore acquire with contend on w. runtime_Semacquire(&rw.readerWait) } runtime_Semrelease(&rw.readerSem) } // RUnlock 解读锁操作 func (rw *RWMutex) RUnlock() { n := atomic.AddInt32(&rw.readerCount, -1) if n < 0 { if n == -1 || atomic.LoadUint32(&rw.writerSem) == 0 { panic("sync: RUnlock of unlocked RWMutex") } if atomic.AddUint32(&rw.readerWait, 1) == rw.writerSem>>1 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem) } } }
读锁操作执行流程:
读锁解锁操作执行流程:
Lock 方法实现加写锁操作,Unlock 方法实现释放写锁的操作。写锁的操作与读锁的操作比,稍微复杂一些。
// Lock 加写锁操作 func (rw *RWMutex) Lock() { rw.w.Lock() // Announce to readers there is a writer. r := atomic.AddUint32(&rw.readerSem, rwmutexMaxReaders) - rwmutexMaxReaders if r != 0 { // wait for readers. runtime_Semacquire(&rw.writerSem) } } // Unlock 解写锁操作 func (rw *RWMutex) Unlock() { if atomic.LoadUint32(&rw.readerSem) == rwmutexMaxReaders { // The lock is not read-locked. // We prefer signal (wake one) over broadcast (wake all) to avoid // waking readers in the common case when there's only one. if atomic.CompareAndSwapUint32(&rw.writerSem, 0, rwmutexMaxReaders) { return } // There are waiters or a writer. runtime_Semrelease(&rw.writerSem) } rw.w.Unlock() }
写锁操作执行流程:
写锁释放操作执行流程:
RLocker 返回一个 RLocker 接口,RLocker 接口包含 RLock() 和 RUnlock() 两个方法,可以将其当做一个只读的锁,在实现时也比较简单。
// RLocker 返回一个RWMutex只读锁接口 func (rw *RWMutex) RLocker() RLocker { return (*rlocker)(rw) } type rlocker RWMutex // RLock 加读锁操作实现 func (r *rlocker) RLock() { (*RWMutex)(r).RLock() } // RUnlock 解读锁操作实现 func (r *rlocker) RUnlock() { (*RWMutex)(r).RUnlock() }
读写锁是在多线程环境下用于同步读操作和写操作的一种机制。相比于普通互斥锁(Mutex),读写锁的读操作不会互斥,多个读操作可以同时进行,而读写锁的写操作则是互斥的,只能有一个写操作进行。这种机制可以有效地提高并发性能,特别适用于读多写少的场景。
读写锁的实现原理主要依赖于两个计数器:读计数器和写计数器。读计数器用于记录当前有多少个读操作正在进行,写计数器则用于记录当前有多少个写操作正在进行。当读计数器为0时,说明没有读操作在进行,此时写操作可以进行。而当写计数器为0时,说明没有写操作在进行,此时读操作可以进行。
在读操作时,读写锁首先检查写计数器的值是否大于0,如果大于0,则表示有写操作正在进行,读操作需要等待。如果写计数器为0,则将读计数器的值加1,表示有一个读操作正在进行。
在写操作时,读写锁首先检查读计数器和写计数器的值是否都为0,如果有任一计数器的值大于0,则表示有其他读操作或写操作正在进行,写操作需要等待。如果两个计数器的值都为0,则将写计数器的值加1,表示有一个写操作正在进行。
在读操作和写操作完成后,对应的计数器的值会减1。
下面是一个简单的Golang版本的读写锁的实现代码:
type RWLock struct { readCount int writeCount int mutex sync.Mutex readCond sync.Cond writeCond sync.Cond } func (rw *RWLock) Init() { rw.readCond.L = &rw.mutex rw.writeCond.L = &rw.mutex } func (rw *RWLock) RLock() { rw.mutex.Lock() for rw.writeCount > 0 { rw.readCond.Wait() } rw.readCount++ rw.mutex.Unlock() } func (rw *RWLock) RUnlock() { rw.mutex.Lock() rw.readCount-- if rw.readCount == 0 { rw.writeCond.Signal() } rw.mutex.Unlock() } func (rw *RWLock) WLock() { rw.mutex.Lock() for rw.readCount > 0 || rw.writeCount > 0 { rw.writeCond.Wait() } rw.writeCount++ rw.mutex.Unlock() } func (rw *RWLock) WUnlock() { rw.mutex.Lock() rw.writeCount-- rw.readCond.Signal() rw.writeCond.Signal() rw.mutex.Unlock() }
在上面的代码中,我们使用了Golang的sync.Mutex
和sync.Cond
来实现读写锁。sync.Mutex
是一个互斥锁,用于保护共享变量的读写操作。sync.Cond
是一个条件变量,用于协调读操作和写操作。
在读操作的RLock
方法中,首先获取锁,然后判断是否有写操作正在进行,如果有,则调用条件变量的Wait
方法等待,直到写计数器为0。然后将读计数器加1,并释放锁。
在读操作的RUnlock
方法中,获取锁,将读计数器减1,如果读计数器为0,则唤醒写操作的等待者,并释放锁。
在写操作的WLock
方法中,首先获取锁,然后判断是否有其他读操作或写操作正在进行,如果有,则调用条件变量的Wait
方法等待,直到读计数器和写计数器都为0。然后将写计数器加1,并释放锁。
在写操作的WUnlock
方法中,获取锁,将写计数器减1,然后分别唤醒读操作和写操作的等待者,并释放锁。
这样,我们就实现了一个简单的读写锁,可以在多线程环境下安全地进行读操作和写操作。
读写锁是一种常见的锁机制,它允许多个读操作同时进行,但是写操作需要独占锁。当写操作进行时,其他读写操作都不能进行,直到写操作完成。在 Go 语言中,读写锁被封装在 sync 包中,它包含了 RWMutex 类型。
当一个 goroutine 想要进行 RWMutex 的写操作时,它必须首先获得锁。如果此时存在其他的 goroutine 正在读或写这个锁,则会被阻塞,直到该 goroutine 成功获得锁进行写操作。否则,如果没有任何一个 goroutine 正在使用该锁,那么这个 goroutine 立即可以使用该锁进行���操作。因为写操作是独占锁,所以在写操作完成前,其他的 goroutine 都无法访问该锁。
RWMutex 实际上维护了两个互斥锁 —— 一个用于读操作,一个用于写操作。在读操作的时候,如果没有任何的写操作,那么可以多个 goroutine 同时获得锁并且进行读操作,因为读操作不会改变被锁定的资源。
当一个 goroutine 想要进行 RWMutex 的写操作时,它必须等到没有任何的读或写操作正在进行,然后才可以进行写操作。因此,写操作是独占锁,要求对当前被锁定的资源实现互斥访问。
在 Go 语言中,RWMutex 的写操作是通过 RWMutex 类型的 Lock() 函数实现的。Lock() 函数实际上就是通过维护读写锁的两个互斥锁,来实现写操作的独占锁。具体实现方法如下所示:
type RWMutex struct { // 当前持有锁的总数,可以是 0、1 或者其他非负整数 w Mutex readerSem uint32 readerCount int32 readerWait int32 } func (rw *RWMutex) Lock() { rw.w.Lock() // 当前没有任何的读操作和写操作,可以获得写锁 // writerSem = 1 表示当前有 goroutine 正在进行写操作 // readerBenign/readerConflict 表示当前是否存在读操作,-1 表示不存在读操作 writerSem, readerBenign, readerConflict := rw.readerCount>>maxRWShift, rw.readerCount&readerBenignMask, rw.readerCount&readerConflictMask if writerSem == 0 && readerConflict == 0 { // 没有 goroutine 读或写当前资源 if atomic.CompareAndSwapInt32(&rw.readerCount, readerBenign, writerLocked|readerBenign) { return } } ......
在上述代码中,首先通过写锁来保护当前共享资源,然后通过锁的状态来判断当前是否允许进行写锁修改操作。如果当前没有任何的读操作和写操作,那么可以将写锁的状态修改为 1(writerSem = 1),表示当前有一个 goroutine 正在进行写操作。
读写锁是一种常用的线程同步机制,而对于大数据量或高并发场景下的读写锁性能表现,则是我们常常需要关注的。在本节中,我们将会进行读写锁的性能测试,分析读写锁的性能表现。
为了保证实验的可靠性,我们需要先在合适的环境中进行测试。在本次测试中,我们使用了一台配备了 Intel® Core™ i7-11500H CPU @ 2.50GHz 处理器和 16GB 内存的计算机,并使用 Go 1.16 版本进行测试。
在本次测试中,我们将分别测试多个线程同时访问共享资源的情况下,读写锁和互斥锁的性能表现。在测试中,我们将会开启多个 goroutine 来模拟多线程的场景。我们将会测试下列场景:
在每个测试场景中我们均会记录如下数据项:
下面是对于上述场景的测试结果统计。在测试中,我们均使用了相同的测试数据(长度为 50000 的整型数组)。
通过测试结果可以看出,在读写混合场景中,读写锁性能优于互斥锁。而在单一读或单一写的场景中,互斥锁的性能则更优。这是因为在单一操作场景中,读写锁需要保证读取和写入操作互不干扰,增加了额外的开销。
此外,我们还可以看出,在并发度较高的情况下,读写锁的性能较互斥锁更优。这是因为在较高的并发度下,读写锁可以让更多的 goroutine 进行读取操作,提高了整体的性能表现。
在实际使用中需要根据具体场景来判断适合使用的锁。
在本节中,我们将介绍一个基于读写锁实现的并发安全的数据结构示例。我们将实现一个简单的计数器,它支持并发地进行计数操作。
在多个goroutine并发地对计数器进行操作时,如果不使用并发控制机制,会导致竞态条件(race condition)的发生,导致计数结果的不确定性和不正确性。通过使用读写锁,我们可以保证在多个读操作的情况下不会出现数据竞争,而仅当写操作发生时才会阻塞其他的读写操作。
首先,我们定义一个结构体 Counter
,其中包含一个计数值和一个读写锁:
type Counter struct {
count int
mutex sync.RWMutex
}
接下来,我们将实现 Counter
结构体的三个方法:Increment()
、Decrement()
和 GetValue()
。
Increment()
方法Increment()
方法用于将计数器的值增加1。在此方法中,我们使用写锁来保护对计数器的写操作:
func (c *Counter) Increment() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.count++
}
Decrement()
方法Decrement()
方法用于将计数器的值减少1。同样地,我们使用写锁来保护对计数器的写操作:
func (c *Counter) Decrement() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.count--
}
GetValue()
方法GetValue()
方法用于获取当前计数器的值。在此方法中,我们使用读锁来保护对计数器的读操作:
func (c *Counter) GetValue() int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.count
}
下面是一个使用我们实现的并发安全计数器的示例代码:
func main() { counter := Counter{} // 使用多个goroutine并发地增加计数器的值 for i := 0; i < 10; i++ { go func() { counter.Increment() }() } // 使用多个goroutine并发地减少计数器的值 for i := 0; i < 5; i++ { go func() { counter.Decrement() }() } // 等待所有goroutine执行完毕 time.Sleep(time.Second) // 打印最后的计数器值 fmt.Println("Final counter value:", counter.GetValue()) }
在上面的示例代码中,我们创建了一个计数器对象,并使用多个goroutine并发地增加和减少计数器的值。通过读写锁的应用,我们可以实现安全的并发计数操作,确保最后的计数器值是正确的。
本文介绍了如何在Go语言中使用读写锁的应用实例。通过实现一个并发安全的计数器,我们展示了如何使用读写锁来保护读写操作,实现高效且安全的并发访问。使用读写锁可以提升程序的性能,并避免竞态条件的发生。读写锁是Go语言中重要的并发原语,对于实现并发安全的数据结构非常有帮助。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。