Golang: 使用 Atomic 减少互斥锁的使用

Published: 2022-02-26

Tags: Golang

本文总阅读量

Golang 提供了内存同步机制,在共享内存场景,使用互斥锁(Mutex)可以对抗数据竞争,在提供两种互斥锁之外,也可以通过原子化操作(Atomic)来解决问题, 相较于使用互斥锁,原子化操作拥有更好的性能。

数据竞争示例

这个简单的示例模拟了一个不断变化的配置,通过多个协程来读取。

package main

import (
  "fmt"
  "sync"
)

type Config struct {
  a []int
}

func main() {
  cfg := &Config{}

  // Writer
  go func () {
    i := 0
    for {
      i++
      cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}
    }
  }()

  // Reader
  var wg sync.WaitGroup
  for n:= 0; n < 4; n++ {
    wg.Add(1)
    go func() {
      for n := 0; n < 1000; n++ {
        fmt.Println(cfg)
      }
      wg.Done()
    }()
  }
  wg.Wait()
}

以下是部分结果

...
&{[250463 250467 250470 250473 250476 250479]}
&{[250524 250526 250529 250532 250536 250539]}
&{[250556 250561 250564 250568 250571 250574]}

按照预期,数据应该是 1 2 3 4 5 6 这样连续的数值,而现在的结果跳跃且不确定,进一步说明以下赋值语句不是原子操作,在赋值过程中,变量 i 的值已经发生了变化。

cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}

解决方案一:互斥锁(Mutex)

使用互斥锁解决数据竞争问题

test-mutex.go

package main

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

type Config struct {
  a []int
}

func main() {
  start := time.Now()
  cfg := &Config{}

  var lock sync.RWMutex

  // Writer
  go func () {
    i := 0
    for {
      i++
      lock.Lock()
      cfg.a = []int{i, i+1, i+2, i+3, i+4, i+5}
      lock.Unlock()
    }
  }()

  // Reader
  var wg sync.WaitGroup
  for n:= 0; n < 4; n++ {
    wg.Add(1)
    go func() {
      for n := 0; n < 1000; n++ {
        lock.RLock()
        fmt.Println(cfg)
        lock.RUnlock()
      }
      wg.Done()
    }()
  }
  wg.Wait()
  end := time.Now()
  fmt.Println(end.Sub(start))
}

部分结果如下,可以看到同一组配置的数值符合预期。

...
&{[1028 1029 1030 1031 1032 1033]}
&{[1029 1030 1031 1032 1033 1034]}
&{[1030 1031 1032 1033 1034 1035]}

使用互斥锁简单粗暴,在需要保障原子化操作的部分加锁,在多个协程的场景下就能避免数据竞争,一般情况下使用锁也是推荐的方式,足够简单且不易出错。

同时,Golang 提供的 atomic 包也值得我们学习和了解,原子操作比锁的性能更好,在性能要求高的场景下是更好的选择。

解决方案二:原子化操作(atomic)

使用 atomic 包提供的方法可以方便的实现原子化操作。

test-atomic.go

package main

import (
  "time"
  "fmt"
  "sync"
  "sync/atomic"
)

type Config struct {
  a []int
}

func main() {
  start := time.Now()
  var v atomic.Value

  // Writer
  go func () {
    i := 0
    for {
      i++
      cfg := &Config{
        a: []int{i, i+1, i+2, i+3, i+4, i+5},
      }
      v.Store(cfg)
    }
  }()

  // Reader
  var wg sync.WaitGroup
  for n:= 0; n < 4; n++ {
    wg.Add(1)
    go func() {
      for n := 0; n < 1000; n++ {
        cfg := v.Load()
        fmt.Println(cfg)
      }
      wg.Done()
    }()
  }
  wg.Wait()
  end := time.Now()
  fmt.Println(end.Sub(start))
}

结果连续,符合预期

...
&{[629131 629132 629133 629134 629135 629136]}
&{[629183 629184 629185 629186 629187 629188]}
&{[629210 629211 629212 629213 629214 629215]}

性能比较

将 Mutex 和 Atomic 版本的代码 fmt.Println打印去掉,执行可以观察到性能差异

code/go-proj/test via 🐹 v1.17.4
➜ go run test-mutex.go
961.007µs

code/go-proj/test via 🐹 v1.17.4
➜ go run test-atomic.go
42.664µs

参考