深入理解 go RWMutex

news2024/11/18 15:49:18

在上一篇文章《深入理解 go Mutex》中,
我们已经对 go Mutex 的实现原理有了一个大致的了解,也知道了 Mutex 可以实现并发读写的安全。
今天,我们再来看看另外一种锁,RWMutex,有时候,其实我们读数据的频率要远远高于写数据的频率,
而且不同协程应该可以同时读取的,这个时候,RWMutex 就派上用场了。

RWMutex 的实现原理和 Mutex 类似,只是在 Mutex 的基础上,区分了读锁和写锁:

  • 读锁:只要没有写锁,就可以获取读锁,多个协程可以同时获取读锁(可以并行读)。
  • 写锁:只能有一个协程获取写锁,其他协程想获取读锁或写锁都只能等待。

下面就让我们来深入了解一下 RWMutex 的基本使用和实现原理等内容。

RWMutex 的整体模型

正如 RWMutex 的命名那样,它是区分了读锁和写锁的锁,所以我们可以从读和写两个方面来看 RWMutex 的模型。

下文中的 reader 指的是进行读操作的 goroutine,writer 指的是进行写操作的 goroutine。

读操作模型

我们可以用下图来表示 RWMutex 的读操作模型:

在这里插入图片描述

上图使用了 w.Lock,是因为 RWMutex 的实现中,写锁是使用 Mutex 来实现的。

说明:

  • 读操作的时候可以同时有多个 goroutine 持有 RLock,然后进入临界区。(也就是可以并行读),上图的 G1G2G3 就是同时持有 RLock 的几个 goroutine。
  • 在读操作的时候,如果有 goroutine 持有 RLock,那么其他 goroutine (不管是读还是写)就只能等待,直到所有持有 RLock 的 goroutine 释放锁。
  • 也就是上图的 G4 需要等待 G1G2G3 释放锁之后才能进入临界区。
  • 最后,因为 G5G6 这两个协程获取锁的时机比 G4 晚,所以它们会在 G4 释放锁之后才能进入临界区。

写操作模型

我们可以用下图来表示 RWMutex 的写操作模型:

在这里插入图片描述

说明:

  • 写操作的时候只能有一个 goroutine 持有 Lock,然后进入临界区,释放写锁之前,所有其他的 goroutine 都只能等待。
  • 上图的 G1~G5 表示的是按时间顺序先后获取锁的几个 goroutine。
  • 上面几个 goroutine 获取锁的过程是:
    • G1 获取写锁,进入临界区。然后 G2G3G4G5 都在等待。
    • G1 释放写锁之后,G2G3 可以同时获取读锁,进入临界区。然后 G3G4G5 都在等待。
    • G2G3 可以同时获取读锁,进入临界区。然后 G4G5 都在等待。
    • G2G3 释放读锁之后,G4 获取写锁,进入临界区。然后 G5 在等待。
    • 最后,G4 释放写锁,G5 获取读锁,进入临界区。

基本用法

RWMutex 中包含了以下的方法:

  • Lock:获取写锁,如果有其他 goroutine 持有读锁或写锁,那么就会阻塞等待。
  • Unlock:释放写锁。
  • RLock:获取读锁,如果有其他 goroutine 持有写锁,那么就会阻塞等待。
  • RUnlock:释放读锁。

其他不常用的方法:

  • RLocker:返回一个读锁,该锁包含了 RLockRUnlock 方法,可以用来获取读锁和释放读锁。
  • TryLock: 尝试获取写锁,如果获取成功,返回 true,否则返回 false。不会阻塞等待。
  • TryRLock: 尝试获取读锁,如果获取成功,返回 true,否则返回 false。不会阻塞等待。

一个简单的例子

我们可以通过下面的例子来看一下 RWMutex 的基本用法:

package mutex

import (
	"sync"
	"testing"
)

var config map[string]string
var mu sync.RWMutex

func TestRWMutex(t *testing.T) {
	config = make(map[string]string)

	// 启动 10 个 goroutine 来写
	var wg1 sync.WaitGroup
	wg1.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			set("foo", "bar")
			wg1.Done()
		}()
	}

	// 启动 100 个 goroutine 来读
	var wg2 sync.WaitGroup
	wg2.Add(100)
	for i := 0; i < 100; i++ {
		go func() {
			get("foo")
			wg2.Done()
		}()
	}

	wg1.Wait()
	wg2.Wait()
}

// 获取配置
func get(key string) string {
	// 获取读锁,可以多个 goroutine 并发读取
	mu.RLock()
	defer mu.RUnlock()

	if v, ok := config[key]; ok {
		return v
	}

	return ""
}

// 设置配置
func set(key, val string) {
	// 获取写锁
	mu.Lock()
	defer mu.Unlock()

	config[key] = val
}

上面的例子中,我们启动了 10 个 goroutine 来写配置,启动了 100 个 goroutine 来读配置。
这跟我们现实开发中的场景是一样的,很多时候其实是读多写少的。
如果我们在读的时候也使用互斥锁,那么就会导致读的性能非常差,因为读操作一般都不会有副作用的,但是如果使用互斥锁,那么就只能一个一个的读了。

而如果我们使用 RWMutex,那么就可以同时有多个 goroutine 来读取配置,这样就可以大大提高读的性能。
因为我们进行读操作的时候,可以多个 goroutine 并发读取,这样就可以大大提高读的性能。

RWMutex 使用的注意事项

在《深入理解 go Mutex》中,我们已经讲过了 Mutex 的使用注意事项,
其实 RWMutex 的使用注意事项也是差不多的:

  • 不要忘记释放锁,不管是读锁还是写锁。
  • Lock 之后,没有释放锁之前,不能再次使用 Lock
  • Unlock 之前,必须已经调用了 Lock,否则会 panic
  • 在第一次使用 RWMutex 之后,不能复制,因为这样一来 RWMutex 的状态也会被复制。这个可以使用 go vet 来检查。

源码剖析

RWMutex 的一些实现原理跟 Mutex 是一样的,比如阻塞的时候使用信号量等,在 Mutex 那一篇中已经有讲解了,这里不再赘述。
这里就 RWMutex 的实现原理进行一些简单的剖析。

RWMutex 结构体

RWMutex 的结构体定义如下:

type RWMutex struct {
	w           Mutex        // 互斥锁,用于保护读写锁的状态
	writerSem   uint32       // writer 信号量
	readerSem   uint32       // reader 信号量
	readerCount atomic.Int32 // 所有 reader 数量
	readerWait  atomic.Int32 // writer 等待完成的 reader 数量
}

各字段含义:

  • w:互斥锁,用于保护读写锁的状态。RWMutex 的写锁是互斥锁,所以直接使用 Mutex 就可以了。
  • writerSem:writer 信号量,用于实现写锁的阻塞等待。
  • readerSem:reader 信号量,用于实现读锁的阻塞等待。
  • readerCount:所有 reader 数量(包括已经获取读锁的和正在等待获取读锁的 reader)。
  • readerWait:writer 等待完成的 reader 数量(也就是获取写锁的时刻,已经获取到读锁的 reader 数量)。

因为要区分读锁和写锁,所以在 RWMutex 中,我们需要两个信号量,一个用于实现写锁的阻塞等待,一个用于实现读锁的阻塞等待。
我们需要特别注意的是 readerCountreaderWait 这两个字段,我们可能会比较好奇,为什么有了 readerCount 这个字段,
还需要 readerWait 这个字段呢?

这是因为,我们在尝试获取写锁的时候,可能会有多个 reader 正在使用读锁,这时候我们需要知道有多少个 reader 正在使用读锁,
等待这些 reader 释放读锁之后,就获取写锁了,而 readerWait 这个字段就是用来记录这个数量的。
Lock 中获取写锁的时候,如果观测到 readerWait 不为 0 则会阻塞等待,直到 readerWait 为 0 之后才会真正获取写锁,然后才可以进行写操作。

读锁源码剖析

获取读锁的方法如下:

// 获取读锁
func (rw *RWMutex) RLock() {
	if rw.readerCount.Add(1) < 0 {
		// 有 writer 在使用锁,阻塞等待 writer 完成
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
	}
}

读锁的实现很简单,先将 readerCount 加 1,如果加 1 之后的值小于 0,说明有 writer 正在使用锁,那么就需要阻塞等待 writer 完成。

释放读锁的方法如下:

// 释放读锁
func (rw *RWMutex) RUnlock() {
	// readerCount 减 1,如果 readerCount 小于 0 说明有 writer 在等待
	if r := rw.readerCount.Add(-1); r < 0 {
		// 有 writer 在等待,唤醒 writer
		rw.rUnlockSlow(r)
	}
}

// 唤醒 writer
func (rw *RWMutex) rUnlockSlow(r int32) {
	// 未 Lock 就 Unlock,panic
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// readerWait 减 1,返回值是新的 readerWait 值
	if rw.readerWait.Add(-1) == 0 {
		// 最后一个 reader 唤醒 writer
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

读锁的实现总结:

  • 获取读锁的时候,会将 readerCount 加 1
  • 如果正在获取读锁的时候,发现 readerCount 小于 0,说明有 writer 正在使用锁,那么就需要阻塞等待 writer 完成。
  • 释放读锁的时候,会将 readerCount 减 1
  • 如果 readerCount 减 1 之后小于 0,说明有 writer 正在等待,那么就需要唤醒 writer。
  • 唤醒 writer 的时候,会将 readerWait 减 1,如果 readerWait 减 1 之后为 0,说明 writer 获取锁的时候存在的 reader 都已经释放了读锁,可以获取写锁了。

·rwmutexMaxReaders算是一个特殊的标识,在获取写锁的时候会将readerCount的值减去rwmutexMaxReaders, 所以在其他地方可以根据 readerCount` 是否小于 0 来判断是否有 writer 正在使用锁。

写锁源码剖析

获取写锁的方法如下:

// 获取写锁
func (rw *RWMutex) Lock() {
	// 首先,解决与其他写入者的竞争。
	rw.w.Lock()
	// 向读者宣布有一个待处理的写入。
	// r 就是当前还没有完成的读操作,等这部分读操作完成之后才可以获取写锁。
	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
	// 等待活跃的 reader
	if r != 0 && rw.readerWait.Add(r) != 0 {
		// 阻塞,等待最后一个 reader 唤醒
		runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
	}
}

释放写锁的方法如下:

// 释放写锁
func (rw *RWMutex) Unlock() {
	// 向 readers 宣布没有活动的 writer。
	r := rw.readerCount.Add(rwmutexMaxReaders)
	if r >= rwmutexMaxReaders { // r >= 0 并且 < rwmutexMaxReaders 才是正常的(r 是持有写锁期间尝试获取读锁的 reader 数量)
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// 如果有 reader 在等待写锁释放,那么唤醒这些 reader。
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// 允许其他的 writer 继续进行。
	rw.w.Unlock()
}

写锁的实现总结:

  • 获取写锁的时候,会将 readerCount 减去 rwmutexMaxReaders,这样就可以区分读锁和写锁了。
  • 如果 readerCount 减去 rwmutexMaxReaders 之后不为 0,说明有 reader 正在使用读锁,那么就需要阻塞等待这些 reader 释放读锁。
  • 释放写锁的时候,会将 readerCount 加上 rwmutexMaxReaders
  • 如果 readerCount 加上 rwmutexMaxReaders 之后大于 0,说明有 reader 正在等待写锁释放,那么就需要唤醒这些 reader。

TryRLock 和 TryLock

TryRLockTryLock 的实现都很简单,都是尝试获取读锁或者写锁,如果获取不到就返回 false,获取到了就返回 true,这两个方法不会阻塞等待。

// TryRLock 尝试锁定 rw 以进行读取,并报告是否成功。
func (rw *RWMutex) TryRLock() bool {
	for {
		c := rw.readerCount.Load()
		// 有 goroutine 持有写锁
		if c < 0 {
			return false
		}
		// 尝试获取读锁
		if rw.readerCount.CompareAndSwap(c, c+1) {
			return true
		}
	}
}

// TryLock 尝试锁定 rw 以进行写入,并报告是否成功。
func (rw *RWMutex) TryLock() bool {
	// 写锁被占用
	if !rw.w.TryLock() {
		return false
	}
	// 读锁被占用
	if !rw.readerCount.CompareAndSwap(0, -rwmutexMaxReaders) {
		// 释放写锁
		rw.w.Unlock()
		return false
	}
	// 成功获取到锁
	return true
}

总结

RWMutex 使用起来比较简单,相比 Mutex 而言,它区分了读锁和写锁,可以提高并发性能。最后,总结一下本文内容:

  • RWMutex 有两种锁:读锁和写锁。
  • 读锁可以被多个 goroutine 同时持有,写锁只能被一个 goroutine 持有。也就是可以并发读,但只能互斥写。
  • 写锁被占用的时候,其他的读和写操作都会被阻塞。读锁被占用的时候,其他的写操作会被阻塞,但是读操作不会被阻塞。除非读操作发生在一个新的写操作之后。
  • RWMutex 包含以下几个方法:
    • Lock:获取写锁,如果有其他的写锁或者读锁被占用,那么就会阻塞等待。
    • Unlock:释放写锁。
    • RLock:获取读锁,如果写锁被占用,那么就会阻塞等待。
    • RUnlock:释放读锁。
  • 也包含了两个非阻塞的方法:
    • TryLock:尝试获取写锁,如果获取不到就返回 false,获取到了就返回 true
    • TryRLock:尝试获取读锁,如果获取不到就返回 false,获取到了就返回 true
  • RWMutex 使用的注意事项跟 Mutex 差不多:
    • 使用之后不能复制
    • Unlock 之前需要有 Lock 调用,否则 panicRUnlock 之前需要有 RLock 调用,否则 panic
    • 不要忘记使用 UnlockRUnlock 释放锁。
  • RWMutex 的实现:
    • 写锁还是使用 Mutex 来实现。
    • 获取读锁和写锁的时候,如果获取不到都会阻塞等待,直到被唤醒。
    • 获取写锁的时候,会将 readerCount 减去 rwmutexMaxReaders,这样就可以直到有写锁被占用。释放写锁的时候,会将 readerCount 加上 rwmutexMaxReaders
    • 获取写锁的时候,如果还有读操作未完成,那么这一次获取写锁只会等待这部分未完成的读操作完成。所有后续的操作只能等待这一次写锁释放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1386154.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度探讨 Golang 中并发发送 HTTP 请求的最佳技术

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在 Golang 领域&#xff0c;并发发送 HTTP 请求…

浊度水质分析仪的功能特性,及其在环境监测中的重要作用

在环境保护和水资源管理领域&#xff0c;对水质的精准监测是确保水体健康、保障公众用水安全的重要环节。其中&#xff0c;浊度作为衡量水体中悬浮物含量的关键参数&#xff0c;其精确测量对于环境评价和治理至关重要。浊度水质分析仪正是这样一款专门针对浊度进行实时在线监测…

【计算机组成-算术逻辑单元】

课程链接&#xff1a;北京大学陆俊林老师的计算机组成原理课 1. 算术运算和逻辑运算 算数运算 逻辑运算 算数逻辑运算的需求 算数运算&#xff1a;两个32-bit数的加减法&#xff0c;结果为一个32-bit数&#xff1b;检查加减法的结果是否溢出逻辑运算&#xff1a;两个32-bit数…

2024谷歌SEO自学基础入门

2024年可能会迎来大航海时代&#xff0c;国内各企业也加速了出海的步伐&#xff01;&#xff01; &#xff08;看总额&#xff0c;今年中国跨境电商&#xff0c;前三季度进出口1.7万亿元人民币&#xff0c;创造了14.4%的增长。 看体量&#xff0c;过去五年&#xff0c;中国跨…

【ESP32最全学习笔记(基础篇)——1.ESP32简介】

ESP32 新手&#xff1f;从这里开始&#xff01; 关于本教程&#xff1a; ESP32 基础篇 1.ESP32简介 ☑ 2.ESP32 Arduino 集成开发环境 3.ESP32 Arduino IDE 2.0 4.VS 代码…

Arduino快速上手esp8266方案开发

认识ESP8266 ESP8266 是 Espressif Systems 生产的 Wi-Fi 片上系统 (SoC)。它非常适合物联网和家庭自动化项目&#xff0c;目前有非常高的市场普及率&#xff0c;还有更加高端的同时支持wifi和蓝牙的双核心芯片ESP32&#xff0c;可以在乐鑫官网查看完整的芯片列表。 ESP8266芯…

Microsoft Remote Desktop for Mac 中文正式版下载 微软远程连接软件

Microsoft Remote Desktop 是一款专为 Mac 用户设计的远程桌面工具&#xff0c;它可以帮助用户通过网络连接到其他计算机&#xff0c;实现远程控制和操作。 软件下载&#xff1a;Microsoft Remote Desktop for Mac 中文正式版下载 该工具支持多种远程连接协议&#xff0c;包括 …

imgaug库指南(23):从入门到精通的【图像增强】之旅

引言 在深度学习和计算机视觉的世界里&#xff0c;数据是模型训练的基石&#xff0c;其质量与数量直接影响着模型的性能。然而&#xff0c;获取大量高质量的标注数据往往需要耗费大量的时间和资源。正因如此&#xff0c;数据增强技术应运而生&#xff0c;成为了解决这一问题的…

DMVAE复现

复现结果–M->S&#xff1a;88.11&#xff0c;S->M&#xff1a;83.75&#xff0c;Joint&#xff1a;44.63&#xff0c;与原文差距在0.5个点内&#xff0c;可以接受 额外信息 第三篇完全复现的论文

Python实现递归最小二乘法回归模型(RecursiveLS算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 RLS主要是在误差平方和最小的原则基础上, 提出一种解析的拟合模型参数的迭代递推公式;可以实现在新的样…

案例126:基于微信小程序的民大食堂用餐综合服务平台

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

warning: GDB: Failed to set controlling terminal: Operation not permitted

运行环境 windows 10 wsl 1 ubuntu 20.04子系统 出现这个问题是因为运行在wsl1上&#xff0c;把系统运行到wsl2就行了。 解决办法 运行到wsl2 https://zhuanlan.zhihu.com/p/337104547 wsl2安装办法 关于编译个一个小问题&#xff0c;在vscode编译的一定要选对编译器&#…

开关电源如何覆铜

开关电源如何覆铜 开关电源覆铜是一个很重要的技术方法&#xff0c;如果没有很好的覆铜&#xff0c;就有可能会造成开关电源芯片的损坏。先介绍常见的开关电源电路&#xff1a; 图 1开关电源电路 从左到右分别是非同步整流Buck电路和同步整流Buck电路&#xff0c;第二排从左到…

Openharmony入门教程 相册问题修复

问题视频&#xff1a; 第五章-保存应用数据 _调用系统相册添加图片_哔哩哔哩_bilibili 跟着视频学习了Openharmony的入门&#xff0c;好不容易到了最后一个章节了&#xff0c;结果遇到了功能无法使用&#xff0c;还是得想办法 问题描述&#xff1a; 按照视频上的代码&#xf…

opencv-py-基础操作

文章目录 阈值分割灰度图效果 二值化效果 二值化取反效果 截取效果 TOZERO效果 TOZERO取反效果 滤波均值滤波高斯滤波中值滤波 图像拼接简单的横向和纵向拼接效果&#xff08;三幅图片分别是均值滤波&#xff0c;高斯滤波&#xff0c;中值滤波&#xff09; 腐蚀与膨胀 阈值分割…

vue下载文件流效果demo(整理)

在 Vue 项目中&#xff0c;你可以使用 FileSaver.js 库来方便地下载文件流。FileSaver.js 封装了不同浏览器的下载方式&#xff0c;使得下载文件更加简单和兼容。以下是一个完整的示例方法&#xff1a; 首先&#xff0c;安装 FileSaver.js 库&#xff1a; <template>&l…

如何通过内网穿透实现公网访问Portainer管理监控Docker容器

文章目录 前言1. 部署Portainer2. 本地访问Portainer3. Linux 安装cpolar4. 配置Portainer 公网访问地址5. 公网远程访问Portainer6. 固定Portainer公网地址 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风…

《如何制作类mnist的金融数据集》——1.数据集制作思路

1&#xff0e;数据集制作思路&#xff08;生成用于拟合金融趋势图像的分段线性函数&#xff09; 那么如何去制作这样的一个类minist的金融趋势曲线数据集呢&#xff1f; 还是如上图所示&#xff0c;为了使类别平均分布&#xff0c;因此可以选取三种“buy”的曲线、三种“sell”…

云渲染农场渲染和自己搭建农场渲染怎么选?哪个更划算?

&#xfeff;当我们面临繁重或紧急的渲染任务时&#xff0c;通常会选择云渲染的解决方案。可能很多人会问&#xff0c;我们是否能够自行建立一个小型的个人农场进行渲染呢&#xff1f;与云渲染农场相比&#xff0c;哪个更划算&#xff1f;更方便&#xff1f;接下来就带大家看看…