浅谈在go语言中的锁

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 【5月更文挑战第11天】本文评估了Go标准库`sync`中的`Mutex`和`RWMutex`性能。`Mutex`包含状态`state`和信号量`sema`,不应复制已使用的实例。`Mutex`适用于保护数据,而`RWMutex`在高并发读取场景下更优。测试显示,小并发时`Mutex`性能较好,但随着并发增加,其性能下降;`RWMutex`的读性能稳定,写性能在高并发时低于`Mutex`。

1 标准库 sync 锁的性能评估

jpegPIA25260.2e16d0ba.fill-400x400-c50.jpg

在标准库Mutex的定义非常简单,它有两个字段 state,sema组成:

    type Mutex struct {
      state int32
      sema  uint32
    }

这两个字段表示

  state 表示当前互斥锁状态。
  sema  用于控制锁状态信号量。

sync同步包 在 src/sync/ 路径,在其中有这样的提示:

    不应该复制哪些包含了此包中类型的值。

    禁止复制首次使用后的Mutex
    禁止复制使用后的RWMutex
    禁止复制使用后的Cond

对mutex实例的复制即是对两个整型字段的复制。

在初始状态,Mutex实例处于 Unlocked状态,state和sema都为0.

实例副本state字段值也为 sync.mutexLocked ,
因此在对其实例复制的副本调用Lock将导致进入阻塞 。

--- 也就是死锁 因为没有任何其他计划调用该副本的Unlock方法,Go不支持递归锁---

那些sync包中类型的实例在首次使用后被复制得到的副本,一旦再被使用将导致不可预期结果,为此在使用sync包的类型时,

推荐通过闭包方式或传递类型实例(或包裹该类型的类型实例)的地址或指针进行,这是sync包最需要注意的。

互斥锁 sync.Mutex,也是编程的同步原语首选,常被用来对结构体对象内部状态,缓存进行保护。 使用最为广泛。

它通常被用以保护结构体内部状态,缓存,是广泛使用的同步原语。

读写锁 RWMutex 有大并发需求的创建,使用读写锁。 RWMutex。

读写锁适合具有一定并发量,并且读取操作明显大于写操作的场景。

2 互斥锁和读写锁例子

一个简单官方例子如下:

  • 创建 锁需要保护的数据变量

      var (
    
        dataOne  = 0
        dataTwo  = 1
        mutexOne sync.Mutex
        mutexTwo sync.RWMutex
      )
    
  • 互斥锁 读取性能

     func BenchmarkReadSyncByMutex(b *testing.B) {
       b.RunParallel(func(pb *testing.PB) {
         for pb.Next() {
           mutexOne.Lock()
           _ = dataOne
           mutexOne.Unlock()
         }
       })
     }
    
    • 互斥锁 写入性能

      func BenchmarkWriteSyncByMutex(b testing.B) {
      b.RunParallel(func(pb
      testing.PB) {

       for pb.Next() {
         mutexOne.Lock()
         dataOne += 1
         mutexOne.Unlock()
       }
      

      })
      }

  • 读写锁 读取性能评估

      func BenchmarkReadSyncByRWMutex(b *testing.B) {
        b.RunParallel(func(pb *testing.PB) {
          for pb.Next() {
            mutexTwo.Lock()
            _ = dataTwo
            mutexTwo.Unlock()
          }
        })
      }
    
  • 读写锁 写性能评估

      func BenchmarkWriteSyncByRWMutex(b *testing.B) {
        b.RunParallel(func(pb *testing.PB) {
          for pb.Next() {
            mutexTwo.Lock()
            dataTwo += 1
            mutexTwo.Unlock()
          }
        })
      }
    

    执行:

        go test -v -count 2 -bench .  mutex_rw_bench_test.go   -cpu 2,4,8,32,128 >bm.txt
    
  • 结果查看:

        goarch: amd64
        cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx  
        BenchmarkReadSyncByMutex
        BenchmarkReadSyncByMutex-2            30831019          40.23 ns/op
        BenchmarkReadSyncByMutex-2            32428663          42.62 ns/op
        BenchmarkReadSyncByMutex-4            10713606         114.1 ns/op
        BenchmarkReadSyncByMutex-4            10344114          98.16 ns/op
        BenchmarkReadSyncByMutex-8            10293854         116.5 ns/op
        BenchmarkReadSyncByMutex-8            10168749         116.9 ns/op
        BenchmarkReadSyncByMutex-32           11110328         111.3 ns/op
        BenchmarkReadSyncByMutex-32           10753728         108.3 ns/op
        BenchmarkReadSyncByMutex-128          12562038          98.00 ns/op
        BenchmarkReadSyncByMutex-128          12499010          96.89 ns/op
        BenchmarkWriteSyncByMutex
        BenchmarkWriteSyncByMutex-2           17350693          67.81 ns/op
        BenchmarkWriteSyncByMutex-2           15188412          66.77 ns/op
        BenchmarkWriteSyncByMutex-4            9374296         125.0 ns/op
        BenchmarkWriteSyncByMutex-4           10168714         126.8 ns/op
        BenchmarkWriteSyncByMutex-8            9916609         119.1 ns/op
        BenchmarkWriteSyncByMutex-8            9755517         121.1 ns/op
        BenchmarkWriteSyncByMutex-32          10713538         113.9 ns/op
        BenchmarkWriteSyncByMutex-32          10568701         113.5 ns/op
        BenchmarkWriteSyncByMutex-128         11649591         102.3 ns/op
        BenchmarkWriteSyncByMutex-128         11973096         102.5 ns/op
        BenchmarkReadSyncByRWMutex
        BenchmarkReadSyncByRWMutex-2          13524128         102.7 ns/op
        BenchmarkReadSyncByRWMutex-2          11999124         101.4 ns/op
        BenchmarkReadSyncByRWMutex-4           8391038         145.8 ns/op
        BenchmarkReadSyncByRWMutex-4          14412699         126.1 ns/op
        BenchmarkReadSyncByRWMutex-8          10525567         116.3 ns/op
        BenchmarkReadSyncByRWMutex-8          10255752         116.4 ns/op
        BenchmarkReadSyncByRWMutex-32         10255778         117.3 ns/op
        BenchmarkReadSyncByRWMutex-32         10208638         117.9 ns/op
        BenchmarkReadSyncByRWMutex-128        10810089         111.0 ns/op
        BenchmarkReadSyncByRWMutex-128        11110348         108.1 ns/op
        BenchmarkWriteSyncByRWMutex
        BenchmarkWriteSyncByRWMutex-2         12499010          91.11 ns/op
        BenchmarkWriteSyncByRWMutex-2         11999124          99.52 ns/op
        BenchmarkWriteSyncByRWMutex-4          7842598         147.7 ns/op
        BenchmarkWriteSyncByRWMutex-4          7946450         151.0 ns/op
        BenchmarkWriteSyncByRWMutex-8         10210080         118.1 ns/op
        BenchmarkWriteSyncByRWMutex-8         10168724         115.7 ns/op
        BenchmarkWriteSyncByRWMutex-32         9835380         119.9 ns/op
        BenchmarkWriteSyncByRWMutex-32        10339772         117.5 ns/op
        BenchmarkWriteSyncByRWMutex-128       10908296         109.5 ns/op
        BenchmarkWriteSyncByRWMutex-128       10810030         109.9 ns/op
        PASS
    

3 小结

简单分析如下:

1 在小并发量时,互斥锁性能更好,并发量增大,互斥锁竞争激烈,导致加锁和解锁性能下降,
  但是最后也恒定在最好记录的2倍左右。
2 读写锁的读锁性能并未随着并发量增大而性能下降,始终在恒定值.
3 并发量较大时,读写锁的写锁性能比互斥锁,读写锁的读锁都差,并且随着并发量增大,写锁性能有继续下降趋势。

多个例程goroutine可以同时持有读锁,从而减少在锁竞争等待的时间,

而互斥锁即便为读请求,同一时刻也只能有一个例程持有锁,其他goroutine被阻塞在加锁操作等待被调度。

由于处于for循环测试中,需要注意的是,不能在 unlock时使用 defer,

  b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
        mutexTwo.Lock()
        dataTwo += 1
        defer mutexTwo.Unlock()
      }
    })

如此在并发执行时,函数不会退出,defer得不到执行,将导致全部死锁。

目录
相关文章
|
1天前
|
Go 开发者
探索Go语言的并发编程模型
通过实例详细介绍了Go语言中的并发编程模型,包括goroutine、channel的基本使用和最佳实践。深入剖析如何利用Go的并发特性提高程序性能和效率,适用于初学者和有一定经验的开发者。
|
3天前
|
Go Python
go语言调用python脚本
go语言调用python脚本
7 0
|
5天前
|
负载均衡 算法 Java
【面试宝藏】Go语言运行时机制面试题
探索Go语言运行时,了解goroutine的轻量级并发及GMP模型,包括G(协程)、M(线程)和P(处理器)。GMP调度涉及Work Stealing和Hand Off机制,实现负载均衡。文章还讨论了从协作到基于信号的抢占式调度,以及GC的三色标记算法和写屏障技术。理解这些概念有助于优化Go程序性能。
23 4
|
6天前
|
JSON Go 数据格式
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(4)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
Java 编译器 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
存储 安全 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(2)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
Java Go 索引
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
安全 Go 开发者
Go语言中的空值与零值有什么区别?
在Go语言中,`nil`和零值有显著区别。`nil`用于表示指针、通道等类型的“无”或“不存在”,而零值是类型的默认值,如数字的0,字符串的`''`。`nil`常用于未初始化的变量或错误处理,零值用于提供初始值和避免未初始化的使用。理解两者差异能提升代码质量和稳定性。
|
8天前
|
Go
如何理解Go语言中的值接收者和指针接收者?
Go语言中,函数和方法可使用值或指针接收者。值接收者是参数副本,内部修改不影响原值,如示例中`ChangeValue`无法改变`MyStruct`的`Value`。指针接收者则允许修改原值,因为传递的是内存地址。选择接收者类型应基于是否需要修改参数,值接收者用于防止修改,指针接收者用于允许修改。理解这一区别对编写高效Go代码至关重要。
|
9天前
|
缓存 Java Go
如何用Go语言构建高性能服务
【6月更文挑战第8天】Go语言凭借其并发能力和简洁语法,成为构建高性能服务的首选。本文关注使用Go语言的关键设计原则(简洁、并发、错误处理和资源管理)、性能优化技巧(减少内存分配、使用缓存、避免锁竞争、优化数据结构和利用并发模式)以及代码示例,展示如何构建HTTP服务器。通过遵循这些原则和技巧,可创建出稳定、高效的Go服务。