sync 包的使用与原理之 RWMutex

160 阅读2分钟

1. 前言

在并发过程中,多个线程或 goroutine 可能同时操作同一内存区域,导致出现竞争问题。为保持内存一致性,Go 的 sync 包提供了常见的并发编程原语。其中包括:Mutex、RWMutex、WaitGroup、Once 和 Pool 等。

2. RWMutex

RWMutex 适用于读多写少的场景,在同一时刻,有且只能有一个 goroutine 获取该锁。writer 比 reader 获取锁的优先级更高,writer 尝试获取锁时,将会阻塞所有新到来的 reader,直至 writer 获取到锁且释放掉。

2.1 使用

package main

import (
	"sync"
)

func main() {
	mu := sync.RWMutex{}
	mu.RLock()
	fmt.Println("Read lock success")
	mu.RUnlock()
	mu.Lock()
	fmt.Println("Write lock success")
	mu.Unlock()
}

输出结果如下:

Read lock success
Write lock success

2.2 实现原理

移除打印输出内容,生成对应的汇编内容如下所示:

TEXT "".main(SB) gofile../Users/liangjinsi/Documents/workspace/wenote/go/source/sync/main.go
  main.go:7             0x602                   493b6610                CMPQ 0x10(R14), SP
  main.go:7             0x606                   7669                    JBE 0x671
  main.go:7             0x608                   4883ec20                SUBQ $0x20, SP
  main.go:7             0x60c                   48896c2418              MOVQ BP, 0x18(SP)
  main.go:7             0x611                   488d6c2418              LEAQ 0x18(SP), BP
  main.go:8             0x616                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:type.sync.RWMutex
  main.go:8             0x61d                   0f1f440000              NOPL 0(AX)(AX*1)
  main.go:8             0x622                   e800000000              CALL 0x627              [1:5]R_CALL:runtime.newobject<1>
  main.go:8             0x627                   4889442410              MOVQ AX, 0x10(SP)
  main.go:8             0x62c                   48c70000000000          MOVQ $0x0, 0(AX)
  main.go:8             0x633                   488d4808                LEAQ 0x8(AX), CX
  main.go:8             0x637                   440f1139                MOVUPS X15, 0(CX)
  main.go:9             0x63b                   488b442410              MOVQ 0x10(SP), AX
  main.go:9             0x640                   6690                    NOPW
  main.go:9             0x642                   e800000000              CALL 0x647              [1:5]R_CALL:sync.(*RWMutex).RLock
  main.go:10            0x647                   488b442410              MOVQ 0x10(SP), AX
  main.go:10            0x64c                   e800000000              CALL 0x651              [1:5]R_CALL:sync.(*RWMutex).RUnlock
  main.go:11            0x651                   488b442410              MOVQ 0x10(SP), AX
  main.go:11            0x656                   e800000000              CALL 0x65b              [1:5]R_CALL:sync.(*RWMutex).Lock
  main.go:12            0x65b                   488b442410              MOVQ 0x10(SP), AX
  main.go:12            0x660                   6690                    NOPW
  main.go:12            0x662                   e800000000              CALL 0x667              [1:5]R_CALL:sync.(*RWMutex).Unlock
  main.go:13            0x667                   488b6c2418              MOVQ 0x18(SP), BP
  main.go:13            0x66c                   4883c420                ADDQ $0x20, SP
  main.go:13            0x670                   c3                      RET
  main.go:7             0x671                   e800000000              CALL 0x676              [1:5]R_CALL:runtime.morestack_noctxt
  main.go:7             0x676                   eb8a                    JMP "".main(SB)

编译器将 sync.RWMutex 解析成 type.sync.RWMutex 类型, RWMutext 是不可复制的

2.2.1 RWMutex 定义

type.sync.RWMutex 结构定义在 /src/sync/rwmutex.go 文件中,具体内容如下:

const rwmutexMaxReaders = 1 << 30

type RWMutex struct {
	w           Mutex  // 互斥锁,保护写操作
	writerSem   uint32 // 写信号
	readerSem   uint32 // 读信号
	readerCount int32  // 读操作计数
	readerWait  int32  // writer 等待完成的 reader 数量
}
  • rwmutexMaxReaders 表示最大同时支持有 1 << 30 个 reader 在进行读取操作。
  • readerCount 记录当前存在多少个 reader 进行读取操作
  • readerWait 表示当 writer 尝试获取锁时,需要等待多少个 reader 读取完成后才能获取锁。
  • writerSem、readerSem 分别为写信号和读信号量,在 RWMutex 中主要通过 runtime_SemacquireMutex 和 runtime_Semrelease 去获取或释放信号量,在源码中两个 function 的描述如下:
// SemacquireMutex is like Semacquire, but for profiling contended Mutexes.
// If lifo is true, queue waiter at the head of wait queue.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_SemacquireMutex's caller.
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)

// Semrelease atomically increments *s and notifies a waiting goroutine
// if one is blocked in Semacquire.
// It is intended as a simple wakeup primitive for use by the synchronization
// library and should not be used directly.
// If handoff is true, pass count directly to the first waiter.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_Semrelease's caller.
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)

2.2.2 RWMutex.Lock

RWMutex.Lock 实现逻辑如下所示:

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
    // 先解决和其它写者的竞争
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

执行流程如下图所示:

20220626180126

简单概括就是:

  1. 写操作先通过 mutex 互斥竞争获取锁
  2. 若竞争到锁,则将 readerCount 的数量取反,并将当前的 readerCount 数记录到 readerWait, 阻塞新到达的 reader
  3. 当 r != 0 或 readerWait 数和 当前已被阻塞的 reader 数不一致时,表示存在读操作,则等待所有唤醒的 reader 处理完后,对 writerSem 信号量进行加锁。

2.2.3 RWMutex.Unlock

RWMutex.Unlock 实现逻辑如下:

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}
    // 通知读者已经没有活跃的写者了,恢复可以读锁锁定
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		race.Enable()
		throw("sync: Unlock of unlocked RWMutex")
	}
	// 唤醒全部被阻塞的读操作
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 释放互斥锁,允许下一个写操作
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

流程描述如下图所示:

20220626181220

简单概括:

  1. 获取被阻塞的 reader 数量,依次唤醒对应的 reader
  2. 释放 mutex 锁

2.2.4 RWMutex.RLock

RWMutex.RLock 实现逻辑如下:

func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
    // reader 数量加 1,若 readerCount 为负值,表示此时有 writer 等待请求锁
    // writer 的优先级高,所以会把后来的 reader 阻塞
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		// 等待写锁释放
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

流程描述如下:

20220629211552

  1. reader 想获取读锁时,会对 readerCount 进行加一,若加一后 readerCount 数仍小于 0,则表示当前有 writer 在执行中,将会堵塞 reader goroutine 直至 writer 释放。

2.2.5 RWMutex.RUnlock

RWMutex.RUnlock 实现逻辑如下:

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
	// 仍有读操作的时候获得了写锁,就会报错
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		throw("sync: RUnlock of unlocked RWMutex")
	}
	// 如果被 writer 阻塞的 reader 数为 0,表示所有的 reader 都释放了
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

流程描述如下:

20220629211002

  1. 若无写锁,那么 r >= 0 直接放行,称为快路径;
  2. 若有写尝试获取锁中,那么进入慢路径。
  • 若读操作数量已经超过预设值,或仍有读操作进行中加了写锁,则异常。
  • readerWait 减1,若 readerWait 为0,表示已经没有活跃的 reader ,此时释放 writeSem 信号,让获取到写锁的 writer 进入锁定状态。