当前位置:首页 > 技术知识 > 正文内容

Go 语言中的 RWMutex 源码解析、使用场景及应用分析

maynowei2周前 (08-03)技术知识10


Go 语言标准库的 sync 包提供了一些非常重要的并发原语来帮助程序开发者处理并发任务。本文将详细解析 sync 包中的 RWMutex,即读写互斥锁(Reader/Writer Mutex),并结合源码探讨其内部实现机制、典型的应用场景以及如何在并发编程中高效使用它。

1. 概述

RWMutex 是 Go 语言中的一个读写锁,用于解决多个线程/协程同时访问共享资源时的同步问题。与普通的互斥锁 Mutex 不同,RWMutex 支持同时多个读取操作,但在有写操作时,所有读操作和其他写操作都会被阻塞,直到写操作完成为止。这种锁可以显著提高读多写少场景下的性能。

使用场景

  • 读多写少的场景:在大部分操作是读取而不是写入的情况下,RWMutex 提供了显著的性能优势,因为它允许多个读者并发访问,而写操作仍然保持独占锁。
  • 数据共享保护:当多个协程需要并发访问共享的数据结构,如字典、链表等,且这些数据既有读操作又有写操作时,RWMutex 可以防止数据竞态问题。

2. RWMutex基本概念

RWMutex 锁具有以下两种模式:

  • 读锁(RLock):允许多个协程同时读取共享资源,但禁止写入操作。读锁之间可以共享。
  • 写锁(Lock):只有一个协程能够写入共享资源,写锁期间禁止任何其他读或写操作。写锁是独占的。

一个 RWMutex 允许:

  • 多个读者(RLock)同时持有锁,而 只有一个写者(Lock) 可以持有写锁。
  • 如果有写者等待,后续的读者将被阻塞直到写锁释放。

3. 源码解析

接下来我们将详细分析 RWMutex 的源码实现,了解它的内部工作机制。

3.1 RWMutex结构体

type RWMutex struct {
    w           Mutex        // 控制写者之间的竞争,表示是否有写者正在等待或持有锁
    writerSem   uint32       // 用于阻塞写者的信号量
    readerSem   uint32       // 用于阻塞读者的信号量
    readerCount atomic.Int32 // 记录当前持有读锁的读者数量
    readerWait  atomic.Int32 // 记录当前正在等待写操作完成的读者数量
}
  • wMutex 类型,用于控制写者之间的竞争。写锁是互斥的,多个写者不能同时持有写锁,因此使用普通的互斥锁来控制。
  • writerSemreaderSem:这两个字段分别是写者和读者的信号量,用于协程阻塞和唤醒。
  • readerCount:使用 atomic.Int32 来记录当前持有读锁的读者数量。如果为负值,则表明有写者正在等待。
  • readerWait:记录已经获取读锁,但等待写锁完成的读者数量。

3.2 读锁 RLock实现

RLock 用于获取读锁,允许多个读者同时读取数据。实现代码如下:

func (rw *RWMutex) RLock() {
    if rw.readerCount.Add(1) < 0 {
        // 有写者正在等待,当前读者需要等待写者完成
        runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
    }
}
  • rw.readerCount.Add(1):每次调用 RLock 时,readerCount 增加 1,表示一个新的读者进入。若结果小于 0,说明有写者正在等待或持有锁,此时当前读者需要等待写锁释放。
  • runtime_SemacquireRWMutexR:阻塞当前读者,直到写者完成写操作并释放写锁。

3.3 写锁 Lock实现

写锁用于获取写者的独占锁,写锁期间不允许任何其他读或写操作。实现代码如下:

func (rw *RWMutex) Lock() {
    // 首先解决写者之间的竞争
    rw.w.Lock()
    // 将 readerCount 减去一个最大值,标记写操作的进入
    r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
    // 如果有活跃的读者,写者需要等待它们完成
    if r != 0 && rw.readerWait.Add(r) != 0 {
        runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
    }
}
  • rw.w.Lock():先使用普通的互斥锁解决多个写者之间的竞争问题。
  • rw.readerCount.Add(-rwmutexMaxReaders):通过减去一个大值(rwmutexMaxReaders)来表示写者正在获取锁,这使得后续的读者无法进入。
  • runtime_SemacquireRWMutex:如果存在活跃的读者,写者将阻塞,等待读者释放锁。

3.4 读锁释放 RUnlock

func (rw *RWMutex) RUnlock() {
    if rw.readerCount.Add(-1) < 0 {
        rw.rUnlockSlow()
    }
}
  • rw.readerCount.Add(-1):释放一个读锁。如果 readerCount 变为负数,表示有写者正在等待,调用 rw.rUnlockSlow 处理写者的唤醒逻辑。

3.5 写锁释放 Unlock

func (rw *RWMutex) Unlock() {
    // 将 readerCount 加回去,表示写者已经离开
    r := rw.readerCount.Add(rwmutexMaxReaders)
    // 唤醒所有阻塞的读者
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    // 释放写锁
    rw.w.Unlock()
}
  • rw.readerCount.Add(rwmutexMaxReaders):将之前扣减的最大值加回,表示写者已经完成。
  • runtime_Semrelease:唤醒所有在写操作期间阻塞的读者。
  • rw.w.Unlock():释放写者的互斥锁。

4. 使用示例

接下来展示如何在实际项目中使用 RWMutex

package main

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

var (
    rwMutex sync.RWMutex
    count   int
)

func reader(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.RLock()
    fmt.Printf("Reader %d: reading count = %d\n", id, count)
    time.Sleep(time.Second) // 模拟读操作
    rwMutex.RUnlock()
}

func writer(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.Lock()
    count += 1
    fmt.Printf("Writer %d: writing new count = %d\n", id, count)
    time.Sleep(time.Second) // 模拟写操作
    rwMutex.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go reader(i, &wg)
    }

    wg.Add(1)
    go writer(1, &wg)

    for i := 4; i <= 6; i++ {
        wg.Add(1)
        go reader(i, &wg)
    }

    wg.Wait()
}
  • 读操作:多个读者可以同时读取数据,互不影响。
  • 写操作:写者期间,其他读者和写者都必须等待,直到写操作完成。

5. 应用范围分析

  • 场景 1:高并发读操作RWMutex 在读多写少的场景中非常适合使用,因为它允许多个并发的读操作,不会相互阻塞,大大提升了性能。
  • 场景 2:大型数据结构访问:如果一个程序需要经常读取共享数据(如缓存、配置等),但偶尔进行更新操作,那么 RWMutex 是非常合适的。
  • 场景 3:日志系统:在日志记录系统中,通常有大量的读取操作(读取日志文件或内存日志缓冲),但偶尔需要写入新的日志,这时可以使用 RWMutex 来保证读写并发。

6. 性能与设计权衡

虽然 RWMutex 在读多写少的场景中性能优异,但在写操作频繁时,其性能可能反而不如普通的 Mutex,因为写锁会阻塞所有的读者。因此,在频繁写操作的

场景中,应尽量避免使用 RWMutex,而考虑使用普通的 Mutex

RWMutex 是 Go 语言并发编程中一个非常重要的工具,专为读多写少的场景设计。通过允许并发的读操作,它能够大幅提升程序的并发性能。然而,开发者应根据实际场景来选择合适的锁类型,避免在写操作频繁的场景中滥用 RWMutex。

相关文章

Win10系统除了移植Objective C,还要有Swift

IT之家讯Build2015大会最让人震惊的莫属微软推出工具,Win10系统可以移植安卓/iOS应用,特别是后者使用的Objective C编程语言,不过最新消息表明,微软连苹果最新的Swift移植也...

Objective-c单例模式的正确写法「藏」

单例模式在iOS开发中可能算是最常用的模式之一了,但是由于oc本身的语言特性,想要写一个正确的单例模式相对来说比较麻烦,这里我就抛砖引玉来聊一聊iOS中单例模式的设计思路。关于单例模式更多的介绍请参考...

单片机C语言编程,心得都在这里了

单片机写代码总踩坑,头文件被无视,老工程师的经验哪里来?前几天写8x8矩阵键盘的程序,搞了三天代码一直乱报错。后来发现自己连头文件是什么都不清楚,之前写的都是小程序,压根没碰过.h文件。看别人的程序都...

2018年度回顾:挖矿木马为什么会成为病毒木马黑产的中坚力量

一、概述根据腾讯御见威胁情报中心监测数据,2018年挖矿木马样本月产生数量在百万级别,且上半年呈现快速增长趋势,下半年上涨趋势有所减缓。由于挖矿的收益可以通过数字加密货币系统结算,使黑色产业变现链条十...

记录程序第一天挖漏洞的过程(挖漏洞需要学什么语言)

第一个漏洞复现fastjson漏洞说明:在本地这个fastjson会出现问题复现的过程生成一个java文件进行攻击注入的方式javac Exploit.java但是必须编译成.class文件才能执行p...

那些技术—Listview的性能提高篇(listview提高效率)

ListView优化一直是一个老生常谈的问题,不管是面试还是平常的开发中,ListView永远不会被忽略掉,那么这篇文章我们来看看如何最大化的优化ListView的性能。 · 1.在adapter中的...