【Go 互斥锁与读写锁深入浅出】


前言

为什么要有锁

Web后端业务或者一些需要高IO的场景中,一些共享资源往往会被系统快速的修改。比如在电商的多线程的场景下,对一个商品数量的修改,往往是由多个用户同时修改。这些用户可以看作不同的线程,在多核CPU下出现并行修改,在单核CPU下出现并发修改。在Go中最简单的例子即是一个公共资源的数,在多个协程中进行累加。


通过waitgroup原子数可以实现协程在主线程中等待退出,具体代码实现如下

package main

import (
	"fmt"
	"sync"
)

func main() {
	count := 0
	var wg sync.WaitGroup
	// 协程数量
	n := 10
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				count++
			}
		}()
	}
	// 等待所有协程结束
	wg.Wait()
	fmt.Println(count)
}


当然,你的结果可能与我的不一样,这一点正好也说明了,协程并发的不确定性。正因为如此,需要锁机制来避免出现这种不确定性。

为什么会出现这个问题?

在 Go 中,当多个并发 Goroutine 对一个共享变量进行累加操作时,如果没有适当的同步机制,累加结果可能会出现错误。这是因为并发访问导致了竞态条件(race condition)。竞态条件是指多个 Goroutine 同时访问和修改同一个共享变量,从而导致不可预测的结果。
在上述代码中,count++看似是一个简单的操作,但实际上它在CPU的层面上会三个步骤:

  1. 读取 count 的当前值。
  2. 将值加 1
  3. 将新的值写回 count

这三个步骤并不是一个原子的过程,所以多个协程在调度时会打断这个过程,当任意程序在执行第二步时被打断,那么下次到它还是会从第二步开始,并紧接着执行第3步,从而写回一个错误的结果。比如下面这个例子:

  1. Goroutine A 读取 count,值为 5。
  2. Goroutine B 读取 count,值为 5。
  3. Goroutine A 将 count1 并写回,count 变为 6。
  4. Goroutine B 将 count1 并写回,count 仍然是 6,而不是期望的 7。

为了防止竞态条件,我们需要使用同步机制来确保对共享变量的访问是安全的。Go 提供了多种同步原语,最常用的方法就是锁和原子操作,本文将详细探讨go中锁相关的知识点。

都有什么锁?

在开发过程中,互斥锁(Mutex)和读写锁(RWMutex)是常见的同步机制,用于管理并发访问共享资源的方式。它们的作用和使用场景略有不同:

  • 互斥锁 : 互斥锁是最基本的锁类型,用于保护临界区(Critical Section),确保同一时间只有一个线程能够访问共享资源。当一个线程获得了互斥锁后,其他线程必须等待该线程释放锁才能继续执行。
  • 读写锁: 读写锁允许多个线程同时读取共享资源,但是对操作是排他的,即写操作会阻塞其他的读写操作。这种锁机制可以提高并发读取的效率。

如果大家注意到的话,可能会注意到,我们使用了sync.WaitGroup来统计协程之间的操作,这个WaitGroup也参与了对协程的数量统计,为什么不会被协程的并发影响到呢?其实这里涉及了另一种原子操作,在Go中通过调用汇编代码实现原子加的操作,接下来我会简单介绍一下原子加在Golang中是怎么实现的。

waitgroup的具体实现

在上一小节的代码当中,我们通过WaitGroup来实现主线程等待协程处理的功能。这WaitGroup是如何实现的?原子加又是什么操作?好吧,看来我们需要向下挖掘一下了,来探索一些Golang的底层实现吧!

原子加是怎么实现的?

sync.waitgroup.go文件中,存在一个WaitGroup的结构体以及一些方法,其结构如下:

type WaitGroup struct {
	noCopy noCopy
	
	// 用于调整计数器和等待者数量。计数器位于高 32 位,等待者数量位于低 32 位。
	state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
	sema  uint32
}

func (wg *WaitGroup) Add(delta int) {
    // 如果开启了竞态检测,则进行相关处理
	if race.Enabled {
		// 如果 delta < 0,则与 Wait 进行同步减少
		if delta < 0 {
			race.ReleaseMerge(unsafe.Pointer(wg))
		}
		race.Disable()
		defer race.Enable()
	}
    // 更新 WaitGroup 的内部状态
	state := wg.state.Add(uint64(delta) << 32)
	v := int32(state >> 32)  // 得到计数器的当前值
	w := uint32(state)       // 得到等待者的数量
    // 如果开启了竞态检测,并且 delta > 0 且计数器刚好从 0 增加到 delta,则模拟一个读操作
	if race.Enabled && delta > 0 && v == int32(delta) {
		race.Read(unsafe.Pointer(&wg.sema))
	}
    // 检查计数器是否为负数,如果是则抛出异常
	if v < 0 {
		panic("sync: negative WaitGroup counter")
	}
    // 如果存在等待者,并且 delta > 0 且计数器刚好从 0 增加到 delta,则抛出异常,防止 Add 和 Wait 并发调用
	if w != 0 && delta > 0 && v == int32(delta) {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
    // 如果计数器大于 0 或者没有等待者,则直接返回
	if v > 0 || w == 0 {
		return
	}
    // 如果计数器已经为 0 且存在等待者,则进行下面的处理

    // 进行一个简单的一致性检查,以防止 WaitGroup 的错误使用
	if wg.state.Load() != state {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
    // 将等待者数量重置为 0
	wg.state.Store(0)
    // 释放所有等待的 goroutine
	for ; w != 0; w-- {
		runtime_Semrelease(&wg.sema, false, 0)
	}
}

WaitGroup之所以在并发的情况也可以保证结果的正确,主要的原因是atomic.Uint64这个结构提供的Add原子操作,state := wg.state.Add(uint64(delta) << 32)通过Add()的方式,将等待者数量原子操作的加入到前32位中,实现等待者数量记录。
sync\atomic\type.go中,包含了这个结构体以及它的一些主要方法:

// A Uint64 is an atomic uint64. The zero value is zero.
type Uint64 struct {
	_ noCopy
	_ align64
	v uint64
}

...

// 原子性地将 delta 加到 x 当前的值上,并返回加法后的新值 new。这允许并发安全地对一个值进行增加操作。
// Add atomically adds delta to x and returns the new value.
func (x *Uint32) Add(delta uint32) (new uint32) { return AddUint32(&x.v, delta) }

这其中比较重要的方法就是CompareAndSwapAdd,这两者是CAS原子操作实现的关键。在waitgroup中,Add()函数实现了原子加。如果你跟着我的路径找了,会发现到这段代码后就找不到对应的实际代码了。
sync/atomic/doc.go

// AddUint64 atomically adds delta to *addr and returns the new value.
// To subtract a signed positive constant value c from x, do AddUint64(&x, ^uint64(c-1)).
// In particular, to decrement x, do AddUint64(&x, ^uint64(0)).
// Consider using the more ergonomic and less error-prone [Uint64.Add] instead
// (particularly if you target 32-bit platforms; see the bugs section).
func AddUint64(addr *uint64, delta uint64) (new uint64)

此时将会调用\Go\src\runtime\internal\atomic\atomic_amd64.s中的汇编代码。

TEXT ·Xadd64(SB), NOSPLIT, $0-24
	MOVQ	ptr+0(FP), BX          // 将参数 val 的指针加载到寄存器 BX 中
	MOVQ	delta+8(FP), AX        // 将参数 delta 的值加载到寄存器 AX 中
	MOVQ	AX, CX                 // 复制 delta 的值到寄存器 CX 中
	LOCK                         // 锁定总线,确保接下来的指令是原子的
	XADDQ	AX, 0(BX)              // 使用 XADDQ 指令,将 AX 的值加到 BX 指向的内存位置,并将原值存储到 AX 中
	ADDQ	CX, AX                 // 将 CX 中的 delta 值加到 AX 中,AX 现在包含原值加上 delta
	MOVQ	AX, ret+16(FP)         // 将 AX 的值存储到返回值位置
	RET                          // 返回

通过LOCK命令锁定当前的总线,从而保证了XADD的原子操作。所以可以使得在Go层面上出现原子加的操作。

waitgroup是怎么实现等待的?

现在我们已经知道原子加的可行性,那么接下来再回到waitgroup.go那部分中,在等待协程出来的实现中,主要用到了waitgroup的三个方法

// 1. Add
func (wg *WaitGroup) Add(delta int) {
    // 如果开启了竞态检测,则进行相关处理
	if race.Enabled {
		// 如果 delta < 0,则与 Wait 进行同步减少
		if delta < 0 {
			race.ReleaseMerge(unsafe.Pointer(wg))
		}
		race.Disable()
		defer race.Enable()
	}
    // 更新 WaitGroup 的内部状态
	state := wg.state.Add(uint64(delta) << 32)
	v := int32(state >> 32)  // 得到计数器的当前值
	w := uint32(state)       // 得到等待者的数量
    // 如果开启了竞态检测,并且 delta > 0 且计数器刚好从 0 增加到 delta,则模拟一个读操作
	if race.Enabled && delta > 0 && v == int32(delta) {
		race.Read(unsafe.Pointer(&wg.sema))
	}
    // 检查计数器是否为负数,如果是则抛出异常
	if v < 0 {
		panic("sync: negative WaitGroup counter")
	}
    // 如果存在等待者,并且 delta > 0 且计数器刚好从 0 增加到 delta,则抛出异常,防止 Add 和 Wait 并发调用
	if w != 0 && delta > 0 && v == int32(delta) {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
    // 如果计数器大于 0 或者没有等待者,则直接返回
	if v > 0 || w == 0 {
		return
	}
    // 如果计数器已经为 0 且存在等待者,则进行下面的处理

    // 进行一个简单的一致性检查,以防止 WaitGroup 的错误使用
	if wg.state.Load() != state {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
    // 将等待者数量重置为 0
	wg.state.Store(0)
    // 释放所有等待的 goroutine
	for ; w != 0; w-- {
		runtime_Semrelease(&wg.sema, false, 0)
	}
}

// 2. Done
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
	wg.Add(-1)
}

// 3. Wait
// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
	if race.Enabled {
		race.Disable()
	}
	for {
		state := wg.state.Load()
		v := int32(state >> 32)
		w := uint32(state)
		if v == 0 {
			// Counter is 0, no need to wait.
			if race.Enabled {
				race.Enable()
				race.Acquire(unsafe.Pointer(wg))
			}
			return
		}
		// Increment waiters count.
		if wg.state.CompareAndSwap(state, state+1) {
			if race.Enabled && w == 0 {
				// Wait must be synchronized with the first Add.
				// Need to model this is as a write to race with the read in Add.
				// As a consequence, can do the write only for the first waiter,
				// otherwise concurrent Waits will race with each other.
				race.Write(unsafe.Pointer(&wg.sema))
			}
			runtime_Semacquire(&wg.sema)
			if wg.state.Load() != 0 {
				panic("sync: WaitGroup is reused before previous Wait has returned")
			}
			if race.Enabled {
				race.Enable()
				race.Acquire(unsafe.Pointer(wg))
			}
			return
		}
	}
}

对于Add()的方法,主要通过原子加的操作state的前32位(计数器数量)和后32位(等待者数量)。
Done()的方法更简单,则是直接调用了Add()方法,并将参数赋值为1
Wait()方法则是通过for循环实现当前线程的阻塞,通过循环检查 wg.state,确保 WaitGroup 的计数器为0时返回。使用了 CompareAndSwap(CAS 无锁算法) 方法和信号量机制确保线程安全,并结合竞争检测来防止竞争条件。

CAS 无锁算法是什么?

CAS 是一种原子操作,通常是由CPU指令实现的。CAS算法涉及到三个操作数:

  1. 内存位置(Memory Location): 这是要进行比较和交换操作的内存地址。
  2. 预期值(Expected Value): 这是希望内存位置当前包含的值。如果内存位置的当前值与这个预期值相同,CAS操作才会进行交换。如果相同,则将该位置的值更新为新的值。
  3. 新值(New Value): 如果内存位置的当前值与预期值相同,那么CAS操作会将该内存位置的值更新为这个新值。
    CAS操作会原子性地执行以下步骤:
  • 读取内存位置的当前值。
  • 比较内存位置的当前值与预期值。如果两者相同,则继续下一步;否则,操作失败并返回当前值。
  • 交换内存位置的当前值为新值。
  • 返回操作是否成功。

这其实涉及到了一个乐观锁的概念,如果大家感兴趣可以去看看相关的介绍。我之后学习到这方面的知识后,也会考虑总结一下。

原子操作实现累加并发安全

既然Golang有原子累加的操作,那我们就可以尝试一下,使用原子累加来避免并发执行加操作时出现的错误。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	// 构建原子类型
	var count atomic.Int64
	var wg sync.WaitGroup
	timeStart := time.Now()
	// 协程数量
	n := 10
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 10000; j++ {
				// 原子操作+1
				count.Add(1)
			}
		}()
	}
	// 等待所有协程结束
	wg.Wait()
	fmt.Println(count.Load())
	fmt.Println(time.Since(timeStart))
}

这里将每个协程的累加数量调成了10000,并增加了计时,方便之后进行对比,可以看到,结果是符合预期的。

Go 互斥锁(Mutex) 深入浅出

Mutex的基础使用

互斥锁是逻辑最简单一种锁,在Golang中创建一个新的Mutex也比较简单

// 创建一个互斥锁
var mu sync.Mutex

// 加锁
// 如果有其他程序加锁后,此时会阻塞,等待其他程序释放锁
mu.Lock()

// 释放锁
mu.Unlock()

用一个不恰当的比喻来描述,一个屋子(总程序)里只有一个厕所(共享资源),屋子里的人(协程)去排队上厕所,一个人通过关闭厕所门(加锁),防止其他进来跟他抢厕所。上完了厕所就把厕所门打开(释放锁)表示资源没有被占用。从而实现了厕所每次只会有一个人在上,这就是生活中的一个锁机制。虽然例子举得糙,希望大家雅俗共赏哈哈。
把这个过程用图表示如下。


此时假设马桶表示共享资源,门表示锁,当协程1执行mu.Lock(),此时表示把门带上,表示里面有人了。

这个时候其他的协程就只能在外面等待,直到协程1自己打开门(释放锁mu.Unlock(),在外侧的等待队列就将排序在最前的协程执行锁的过程。

这样的话,每个协程就不会出现同时修改共享资源的情况,从而实现共享资源的互斥锁的情形。
按照这个思路,我们来解决刚开始遇到的累加异常问题,通过在协程中每次修改前和修改后的加锁和释放锁,来实现资源不抢占。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	count := 0
	var wg sync.WaitGroup

	// 构建一个互斥锁
	var mu sync.Mutex
	timeStart := time.Now()
	// 协程数量
	n := 10
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 10000; j++ {

				// 2. 加锁
				mu.Lock()
				count++

				// 3. 释放锁
				mu.Unlock()
			}
		}()
	}
	// 等待所有协程结束
	wg.Wait()
	fmt.Println(count)
	fmt.Println(time.Since(timeStart))
}

上述代码中,我们只增加了三行代码,都标识在注释当中。这时结果就是一个符合我们预料的结果了。


时间相比于原子操作锁大概翻了一倍左右。所以互斥锁对于时间成本上,相对来说还是有一点代价的。

互斥锁在Golang中的实现原理

互斥锁实现的代码位于sync/mutex.go中,其主要结构体和方法如下所示:

type Mutex struct {
	state int32  // 锁的状态
	sema  uint32 // 信号量,通常用于等待和通知机制
}

对于Mutex结构体,其主要包含了statesema两个变量,分别用来记录锁的状态和表示信号量。
接下来就是实现加锁和释放锁的两个方法
lock方法

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

可以看到在注释中,有提到两段操作,分别是快速路径和慢速路径:

  • 快路径(Fast Path): 首先尝试快速获取锁。如果 state 当前为 0(表示锁未被占用),使用 atomic.CompareAndSwapInt32 将 state 从 0 更改为 mutexLocked。这是一种原子操作,确保了在多线程环境下的安全性。如果成功,则锁被当前线程获得。
  • 慢路径(Slow Path): 如果锁已经被占用(即 state 不为 0),则调用 m.lockSlow() 进入慢路径,执行等待机制,以确保线程安全。
    这里m.lockSlow方法中将循环对state进行CAS操作,知道检测到锁被释放,才会加锁退出函数。

Unlock方法

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// Fast path: drop lock bit.
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// Outlined slow path to allow inlining the fast path.
		// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
		m.unlockSlow(new)
	}
}
  • 快路径: 使用 atomic.AddInt32state 减去 mutexLocked,尝试释放锁。如果 state 的新值不为 0(即锁还在被其他线程持有),则调用 m.unlockSlow(new) 处理慢路径逻辑。
    m.unlockSlow(new) 方法同样也是通过for循环自旋的检查锁的变化

这样的一个过程就可以实现Golang中互斥锁的实现。

Go 读写锁 (RWMutex) 深入浅出

RWMutex的基础使用

Golang中RWMutex锁的作用是为了优化互斥锁的业务逻辑。在使用互斥锁的时候,可能存在一些业务仅仅去查看共享资源中的数据。为了尽可能的降低锁对这种场景的影响,读写锁就油然而生。

Golang中使用读写锁主要通过如下代码实现

package main

import "sync"

func main() {
	// 创建一个读写锁
	var rwMutex sync.RWMutex

	// 加互斥锁
	rwMutex.Lock()
	// 释放互斥锁
	rwMutex.Unlock()

	// 加读锁
	rwMutex.RLock()
	// 释放读锁
	rwMutex.RUnlock()
}

读写锁总的来说会遵守以下三个规则:

  1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
  2. 在读锁没有全部解锁的情况下,写锁会阻塞直到所有读锁解锁。
  3. 写锁下的情况下,其他协程的读写都会被阻塞,直到写锁解锁。

当然在高版本的go中还有TryLock这些非阻塞加锁方式,因为和本次探讨的内容无关,所以大家有兴趣可以自己尝试一下。
使用读写锁,可以很好的应用在读多写少的场景中,这样写业务执行的写操作,也不会影响其他协程的读操作。如果还要用厕所理论去解释读写锁,那就是在厕所上加一个可以调节透明度的门,此时调节透度的功能就是读写锁。当门透明时,外部可以看到内部状况(读锁),当门不透明时,外部也无法看到内部的情况(写锁)。

此时协程1想上厕所(占用资源),从而占用马桶。因此按照之前的理论,这时他可能会关上门,从而占用了整个厕所(资源),此时协程2即使只是想看眼马桶的样子(读操作)也会被阻塞。假设当前门是可调节透明度的(RW锁),而协程1在使用厕所时,将玻璃门关上,并且开启透明模式(使用读锁),此时协程2可以直接看到马桶的样子(资源值),而且并不会阻塞。

上述过程可以使用如下代码表示:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// 创建一个读写锁
	var rwMutex sync.RWMutex
	var wg sync.WaitGroup
	timeStart := time.Now()
	cnt := 2
	wg.Add(2)
	go func() {
		defer wg.Done()
		rwMutex.RLock()
		// 程序执行中
		fmt.Println("协程 1执行中")
		time.Sleep(time.Second)
		cnt++
		rwMutex.RUnlock()
	}()

	go func() {
		defer wg.Done()
		// 加写锁
		rwMutex.RLock()
		fmt.Println("协程2:", cnt)
		fmt.Println(time.Now().Sub(timeStart))
		// 释放写锁
		rwMutex.RUnlock()
	}()
	wg.Wait()
	fmt.Println("最终结果:", cnt)
	fmt.Println(time.Now().Sub(timeStart))
}


协程1执行中,协程2在0秒内迅速读取了cnt的数值,并没有因为协程1的占用而被阻塞。

读写锁在Golang中的实现原理

首先在sync/rwmutex.go中定义了 RWmutex结构体,

type RWMutex struct {
	// 互斥锁
	w           Mutex        // 如果有待处理的写者,则持有的互斥锁
	writerSem   uint32       // 写者等待读者完成的信号量
	readerSem   uint32       // 读者等待写者完成的信号量
	readerCount atomic.Int32 // 待处理的读者数量 如果值为负,则表示有一个写者在等待。
	readerWait  atomic.Int32 // 离开的读者数量
}

发现其中包含了互斥锁这个变量,那就不难理解为什么读写锁也可以实现互斥锁的功能。
Lock方法

func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
	rw.w.Lock()
	// 通知读者有写者等待
	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
	// 等待活动的读者
	if r != 0 && rw.readerWait.Add(r) != 0 {
		runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

UnLock方法

func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// 通知读者没有活动的写者。
	r := rw.readerCount.Add(rwmutexMaxReaders)
	// 此时表示两次释放读锁,说明锁操作使用有问题
	if r >= rwmutexMaxReaders {
		race.Enable()
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// 解锁被阻塞的读者(如果有)。r的数量表示剩余阻塞的读者
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 允许其他写者继续。
	rw.w.Unlock()
	if race.Enabled {
		race.Enable()
	}
}

所以接下来我们来探究一下读锁是怎么实现的。
RLock方法

// documentation on the RWMutex type.
func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if rw.readerCount.Add(1) < 0 {
		// A writer is pending, wait for it.
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}

  • 增加读者计数: 通过 rw.readerCount.Add(1) 增加读者计数,如果结果小于0,说明有写者在等待,则当前读者需要等待。
  • 等待写者完成: 调用 runtime_SemacquireRWMutexR 等待信号量,直到写者完成。

RUnlock方法

func (rw *RWMutex) RUnlock() {
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	if r := rw.readerCount.Add(-1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}
  • 减少读者计数: 通过 rw.readerCount.Add(-1) 减少读者计数,如果结果小于0,说明有写者在等待,则调用 rw.rUnlockSlow 处理。

rw.rUnlockSlow 方法

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
	if rw.readerWait.Add(-1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

rUnlockSlow 方法处理在读者解锁时遇到写者等待的情况。通过runtime_Semrelease快速通知写锁的执行

总得来说,读写锁通过增加一个原子计数和读信号和写信号来快速判断当前是否存在读者等待或写者等待。当readerCount 为负数时,则理论上表示当前锁中有写锁正在等待。并且sem信号量则可以快速通知读者或写者的阻塞程序,使其快速进入下一阶段(这里细节我没有继续探究了,如果大家想知道更多,可以自行查阅相关代码注释哈)。

文章总结

在本篇博客中,我们探讨了锁机制在程序中的作用,并用一个并发累加的小例子来演示了,无锁情况下并发的不确定性。所以我们开始展开Golang中两种锁的使用的研究。在此期间顺便研究了waitgroup的实现原理,并且通过查询汇编语言文件知道了原子加操作是如何实现的。随后探究了互斥锁和原子操作对并发累加不确定性的解决和性能之间的关系。并探索了互斥锁与读写锁在golang中的源码是如何实现的。

本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值