浅谈在go语言中的锁

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
可观测链路 OpenTelemetry 版,每月50GB免费额度
简介: 【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得不到执行,将导致全部死锁。

目录
相关文章
|
12天前
|
Go
go的并发初体验、加锁、异步
go的并发初体验、加锁、异步
10 0
|
12天前
|
Go
go语言map、实现set
go语言map、实现set
14 0
|
12天前
|
Go
go语言数组与切片
go语言数组与切片
17 0
|
5天前
|
存储 Go API
一个go语言编码的例子
【7月更文挑战第2天】本文介绍Go语言使用Unicode字符集和UTF-8编码。Go中,`unicode/utf8`包处理编码转换,如`EncodeRune`和`DecodeRune`。`golang.org/x/text`库支持更多编码转换,如GBK到UTF-8。编码规则覆盖7位至21位的不同长度码点。
70 1
一个go语言编码的例子
|
8天前
|
JSON 算法 测试技术
在go语言中调试程序
【6月更文挑战第29天】Go语言内置`testing`包支持单元测试、基准测试和模糊测试。`go test`命令可执行测试,如`-run`选择特定测试,`-bench`运行基准测试,`-fuzz`进行模糊测试。
17 2
在go语言中调试程序
|
6天前
|
安全 Go
Go语言的iota关键字有什么用途?
**Go语言中的`iota`是常量生成器,用于在`const`声明中创建递增的常量。`iota`在每个新的`const`块重置为0,然后逐行递增,简化了枚举类型或常量序列的定义。例如,定义星期枚举:** ```markdown ```go type Weekday int const ( Sunday Weekday = iota // 0 Monday // 1 Tuesday // 2 ... ) ``` 同样,`iota`可用于定义不同组的常量,如状态码和标志位,保持各自组内的递增,提高代码可读性。
|
11天前
|
Devops Go 云计算
Go语言发展现状:历史、应用、优势与挑战
Go语言发展现状:历史、应用、优势与挑战
|
2天前
|
监控 搜索推荐 Go
万字详解!在 Go 语言中操作 ElasticSearch
本文档通过示例代码详细介绍了如何在Go应用中使用`olivere/elastic`库,涵盖了从连接到Elasticsearch、管理索引到执行复杂查询的整个流程。
8 0
|
6天前
|
IDE Linux Go
记录一个go语言与IDE之间的问题
【7月更文挑战第1天】本文介绍在IDE中调试Go应用可能遇到的问题。当问题与IDE的自动完成有关,可以试着使用其他编辑器如Linux的vim是否无此问题。这可以验证表明IDE可能不完全兼容最新语言版本,建议使用无自动检测工具临时解决。
22 0
|
10天前
|
编译器 Go C++
必知的技术知识:go语言快速入门教程
必知的技术知识:go语言快速入门教程