Go RWMutex:高并发读多写少场景下的性能优化利器

这篇具有很好参考价值的文章主要介绍了Go RWMutex:高并发读多写少场景下的性能优化利器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

原创文章,如需转载请联系
作者:陈明勇
公众号:Go技术干货

前言

在这篇文章 Go Mutex:保护并发访问共享资源的利器 中,主要介绍了 Go 语言中互斥锁 Mutex 的概念、对应的字段与方法、基本使用和易错场景,最后基于 Mutex 实现一个简单的协程安全的缓存。而本文,我们来看看另一个更高效的 Go 并发原语,RWMutex

准备好了吗?喝一杯你最喜欢的饮料,随着文章一起进入 RWMutex 令人兴奋的世界!

说明:本文使用的代码基于的 Go 版本:1.20.1

RWMutex

读写互斥锁是一种同步原语,它允许多个协程同时访问共享资源,同时确保一次只有一个协程可以修改资源。相较于互斥锁,读写互斥锁在读操作比写操作更频繁的情况下,可以带来更好的性能表现。

Go 语言中,RWMutex 是一种读写互斥锁的实现,它提供了一种简单有效的方式来管理对共享资源的并发访问。它提供了两种类型的锁:读锁写锁

  • 1、读锁(RLock()TryRLock()RUnlock() 方法)

    RWMutex 的读锁是一种共享锁,当一个协程获取了读锁后,其他协程也可以同时获取读锁,从而允许并发的读操作。

  • 2、写锁(Lock()TryLock()Unlock() 方法)

    RWMutex 的写锁是一种独占锁,当一个协程获取了写锁后,其他协程无法获取读锁或写锁,直到该协程释放写锁。在写锁未被释放之前,任何想要获取读锁或写锁的 goroutine 都会被阻塞。

Go RWMutex:高并发读多写少场景下的性能优化利器

RWMutex 结构体介绍

type RWMutex struct {
   w           Mutex        
   writerSem   uint32       // 写操作等待者
   readerSem   uint32       // 读操作等待者 
   readerCount atomic.Int32 // 持有读锁的 goroutine 数量
   readerWait  atomic.Int32 // 请求写锁时,需要等待完成的读锁数量
}

RWMutex 由以下字段组成:

  • w: 为互斥锁,用于实现写操作之间的互斥。
  • writerSem:写操作的信号量。当有 goroutine 请求写操作时,如果有其他的 goroutine 正在执行读操作,则请求写操作的 goroutine 将会被阻塞,直到所有的读操作完成后,通过 writerSem 信号量解除阻塞。
  • readerSem:读操作的信号量。当有 goroutine 请求读操作时,如果此时存在写操作,则请求读操作的 goroutine 将会被阻塞,直到写操作执行完成后,通过 readerSem 信号量解除阻塞并继续执行。
  • readerCount:读操作的goroutine数量,当readerCount为正数时,表示有一个或多个读操作正在执行,如果 readerCount 的值为负数,说明有写操作正在等待。
  • readerWait:写操作的 goroutine 等待读操作完成的数量。当一个写操作请求执行时,如果此时有一个或多个读操作正在执行,则会将读操作的数量记录到readerWait中,并阻塞写操作所在的goroutine。写操作所在的goroutine会一直阻塞,直到正在执行的所有读操作完成,此时readerWait的值将被更新为 0,并且写操作所在的goroutine将被唤醒。

RWMutex 常用方法:

  • Lock():获取写锁,拥有写操作的权限;如果读操作正在执行,此方法将会阻塞,直到所有的读操作执行结束。
  • Unlock():释放写锁,并唤醒其他请求读锁的 goroutine
  • TryLock():尝试获取写锁,如果获取成功,返回 true,否则返回 false,不存在阻塞的情况。
  • RLock():获取读锁,读锁是共享锁,可以被多个 goroutine 获取,但是如果有写操作正在执行或等待执行时,此方法将会阻塞,直到写操作执行结束。
  • RUnlock():释放读锁,如果所有读操作都结束并且有等待执行的写操作,则会唤醒对应的 goroutine
  • TryRlock():尝试获取读锁,如果获取成功,返回 true,否则返回 false,不存在阻塞的情况。

简单读写场景示例

package main

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

type Counter struct {
   value   int
   rwMutex sync.RWMutex
}

func (c *Counter) GetValue() int {
   c.rwMutex.RLock()
   defer c.rwMutex.RUnlock()
   return c.value
}

func (c *Counter) Increment() {
   c.rwMutex.Lock()
   defer c.rwMutex.Unlock()
   c.value++
}
func main() {
   counter := Counter{value: 0}

   // 读操作
   for i := 0; i < 10; i++ {
      go func() {
         for {
            fmt.Println("Value: ", counter.GetValue())
            time.Sleep(time.Millisecond)
         }
      }()
   }

   // 写操作
   for {
      counter.Increment()
      time.Sleep(time.Second)
   }
}

上述代码示例中定义了一个 Counter 结构体,包含一个 value 字段和一个 sync.RWMutex 实例 rwMutex。该结构体还实现了两个方法:GetValue()Increment(),分别用于读取 value 字段的值和对 value 字段的值加一。这两个方法在访问 value 字段时,使用了读写锁来保证并发安全。

main() 函数中,首先创建了一个 Counter 实例 counter,然后启动了 10 个协程,每个协程会不断读取 counter 并打印到控制台上。同时,main() 函数也会不断对 countervalue 值加 1,每次加 1 的操作都会休眠 1 秒钟。由于使用了读写锁,多个读操作可以同时进行,而写操作则会互斥进行,保证了并发安全。

基于 RWMutex 实现一个简单的协程安全的缓存

在 Go Mutex:保护并发访问共享资源的利器 文章中,使用了 Mutex 实现了一个简单的线程安全的缓存,但并不是最优的设计,对于缓存场景,读操作比写操作更频繁,因此使用 RWMutex 代替 Mutex 会更好。

import "sync"

type Cache struct {
   data    map[string]any
   rwMutex sync.RWMutex
}

func NewCache() *Cache {
   return &Cache{
      data: make(map[string]any),
   }
}

func (c *Cache) Get(key string) (any, bool) {
   c.rwMutex.RLock()
   defer c.rwMutex.RUnlock()
   value, ok := c.data[key]
   return value, ok
}

func (c *Cache) Set(key string, value any) {
   c.rwMutex.Lock()
   defer c.rwMutex.Unlock()
   c.data[key] = value
}

上述代码实现了一个协程安全的缓存,通过使用 RWMutex 的读写锁,保证了 Get() 方法可以被多个 goroutine 并发地执行,而且只有在读操作和写操作同时存在时才会进行互斥锁定,有效地提高了并发性能。

RWMutex 易错场景

没有正确的加锁和解锁

为了正确使用读写锁,必须正确使用锁的方法。对于读操作,必须成对使用 RLock()RUnlock() 方法,否则可能会导致程序 panic 或阻塞。

例如:如果缺少 RLock(),直接使用 RUnlock()方法,程序将会 panic,如果缺少 RUnlock() 方法,将会发生阻塞的形象。

同样,对于写操作,必须成对使用 Lock()Unlock() 方法。

最佳实践是使用 defer 来释放锁:为了保证锁总是被释放,即使在运行时错误或提前返回的情况下,也可以在获得锁后立即使用 defer 关键字来调度相应的解锁方法。

rwMutex.RLock()
defer rwMutex.RUnlock()
// 读操作

rwMutex.Lock()
defer rwMutex.Unlock()
// 写操作

重复加锁

重复加锁操作被称为可重入操作。不同于其他一些编程语言的锁实现(例如 JavaReentrantLock),Gomutex 并不支持可重入操作。

由于 RWMutex 内部是基于 Mutex 实现的写操作互斥,如果发生了重复加锁操作,就会导致死锁。这个易错场景在上篇文章中也提到了,还给出了代码示例,感兴趣的小伙伴可以去看看。

读操作内嵌写操作

当有协程执行读操作时,请求执行写操作的协程会被阻塞。如果在读操作中嵌入写操作的代码,写操作将调用 Lock() 方法,从而导致读操作和写操作之间形成相互依赖关系。在这种情况下,读操作会等待写操作完成后才能执行 RUnlock(),而写操作则会等待读操作完成后才能被唤醒继续执行,从而导致死锁的状态。

小结

RWMutexGo 中的一种读写锁实现,它通过读锁允许多个 goroutine 同时执行读操作,当有写操作请求时,必须等待所有读操作执行结束后才能执行写操作。

RWMutex 的设计采用了 Write-preferring 方案,即如果有写操作在等待执行,新来的读操作将会被阻塞,以避免写操作的饥饿问题。

根据 RWMutex 的特性,它适用于 读多写少的高并发场景,可以实现并发安全的读操作,从而减少在锁竞争中的等待时间。

虽然它能够给程序带来了性能的提升,然而,如果使用不当,就可能会导致 panic 或死锁等问题。因此,在使用 RWMutex 时需要特别小心,并避免错误的用法。文章来源地址https://www.toymoban.com/news/detail-406134.html

到了这里,关于Go RWMutex:高并发读多写少场景下的性能优化利器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • go中读写锁(rwmutex)源码解读实现原理

    1、RWMutex读写锁的概念 读写锁也就是我们所使用的RWMutex,其实是对于go本身的mutex做的一个拓展,当一个goroutine获得了读锁后,其他goroutine同样可以获得读锁,但是不能获得写锁。相反,当一个goroutine获得了写锁,其他goroutine既不能读也不能写,互斥的概念。 2、使用场景 适

    2024年02月11日
    浏览(15)
  • 【性能测试】稳定性/并发压力测试的TPS计算+5W并发场景设计...

    1、稳定性测试TPS计算 ①普通计算公式:TPS=总请求数 / 总时间按照需求得到基础数据。 在去年第20周,某平台有5万的浏览量 那么总请求数我们可以估算为5万(1次浏览都至少对应1个请求) 总请求数 = 50000请求数 总时间:由于不知道每个请求的具体时间,我们按照普通方法,

    2024年02月04日
    浏览(20)
  • Go并发:使用sync.Pool来性能优化

    在Go提供如何实现对象的缓存池功能?常用一种实现方式是:sync.Pool, 其旨在缓存已分配但未使用的项目以供以后重用,从而减轻垃圾收集器(GC)的压力。 sync.Pool的结构也比较简单,常用的方法有Get、Put 接着,通过一个简单的例子,来看看是如何使用的 在之前的文章中有提

    2024年02月08日
    浏览(22)
  • Go协程揭秘:轻量、并发与性能的完美结合

    Go协程为并发编程提供了强大的工具,结合轻量级、高效的特点,为开发者带来了独特的编程体验。本文深入探讨了Go协程的基本原理、同步机制、高级用法及其性能与最佳实践,旨在为读者提供全面、深入的理解和应用指导。 关注公众号【TechLeadCloud】,分享互联网架构、云

    2024年02月08日
    浏览(18)
  • 设计模式之并发特定场景下的设计模式 Two-phase Termination(两阶段终止)模式

    在线程1中如何终止线程2? stop()?还是System.exit()?还是其他方式 1.使用stop()不可取 线程对象的stop()方法会直接杀死线程,假设此时使用了线程锁,当此时使用了stop()命令会导致线程锁无法释放,以至于程序出现严重的问题,其中最常见的是死锁。还可能导致资源泄露,因为

    2024年01月21日
    浏览(19)
  • 掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)

    递归函数在Go语言中是一种强大的工具,能够解决许多复杂的问题。除了基本的递归用法外,Go语言还提供了一些高级用法,使得递归函数更加灵活和强大。本文将深入探讨Go语言递归函数的高级用法,包括尾递归优化、并发递归和记忆化递归等。 尾递归优化 尾递归是一种特

    2024年04月10日
    浏览(29)
  • 分布式存储 vs. 全闪集中式存储:金融数据仓库场景下的性能对比

    作者:深耕行业的 SmartX 金融团队 张德敏 近年来随着金融行业的高速发展,经营决策者及监管机构对信息时效性的要求越来越高,科技部门面临诸多挑战。例如,不少金融机构使用数仓业务系统,为公司高层提供日常经营报表,同时支持监管报送等应用。该业务系统通常是

    2024年02月07日
    浏览(20)
  • 掌握Go并发:Go语言并发编程深度解析

    🏷️ 个人主页 :鼠鼠我捏,要死了捏的主页  🏷️ 系列专栏 :Golang全栈-专栏 🏷️ 个人学习笔记,若有缺误,欢迎评论区指正   前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站AI学习网站。 当我们开发一个W

    2024年02月20日
    浏览(23)
  • 高并发环境下的实现与优化策略

    在现代互联网应用中,高并发处理能力是衡量系统性能和稳定性的关键指标之一。尤其对于电商、社交、在线支付等业务场景,面对瞬间涌入的大规模用户请求,如何保证系统的稳定性和响应速度,对技术架构设计与优化提出了极高要求。本文将深入探讨高并发环境下的实现

    2024年04月10日
    浏览(19)
  • 16 Go并发编程(三): Go并发的传统同步机制

    Go 传统同步机制 在《Go并发编程初探》中我们提到同步概念,所谓同步是相对异步而言,即串行相对于并行。 在学习Go通信机制时我们知道管道其实就是并发单元同步方式的一种,基于CSP并发模型,Go在语言原语上使管道作为核心设计,这是Go的设计哲学,也是Go所提倡的同步

    2023年04月08日
    浏览(16)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包