原子操作

atomic包

atomic 提供的原子操作能够确保任一时刻只有一个 goroutine 对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。

atomic常见操作有:

  • 增减
  • 载入
  • 比较并交换
  • 交换
  • 存储

增减操作

在 Go 语言的 sync/atomic 包中,提供了一系列原子操作函数,其中包括增减操作。这些函数能够确保在并发环境中对变量进行安全地增减操作,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic 包中的增减操作:

  1. Add 函数系列

    • AddInt32AddInt64AddUint32AddUint64AddUintptr:这些函数用于对对应类型的变量进行增加操作,并返回增加前的值。函数原型如下:

      1
      2
      3
      4
      5
      
      func AddInt32(addr *int32, delta int32) (new int32)
      func AddInt64(addr *int64, delta int64) (new int64)
      func AddUint32(addr *uint32, delta uint32) (new uint32)
      func AddUint64(addr *uint64, delta uint64) (new uint64)
      func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
  2. Sub 函数系列

    • SubInt32SubInt64:这些函数用于对对应类型的变量进行减少操作,并返回减少前的值。函数原型如下:
      1
      2
      
      func SubInt32(addr *int32, delta int32) (new int32)
      func SubInt64(addr *int64, delta int64) (new int64)
  3. 增减操作注意事项

    • 这些增减操作都是原子的,即在执行过程中不会被中断。

    • 这些函数都接受指向对应类型的变量的指针作为参数。

      第一个参数必须是指针类型的值,通过指针变量可以获取被操作数在内存中的地址,确保同一时间只有一个goroutine能够进行操作。

    • 参数 delta 是要增加或减少的值。

    • 这些函数会返回操作前的值,因此可以用于一些需要获取旧值的场景。

  4. 示例代码

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package main
    
    import (
        "fmt"
        "sync/atomic"
    )
    
    func main() {
        var count int32 = 0
    
        // 增加操作
        old := atomic.AddInt32(&count, 5)
        fmt.Println("Old value:", old) // 输出: Old value: 0
        fmt.Println("New value:", count) // 输出: New value: 5
    
        // 减少操作
        old = atomic.SubInt32(&count, 2)
        fmt.Println("Old value:", old) // 输出: Old value: 5
        fmt.Println("New value:", count) // 输出: New value: 3
    }

多协程变量累加

在Go语言中,可以使用锁和原子操作来实现多个goroutine对同一个变量进行累加操作。

1.加锁的方式

首先,我们先来看如何使用锁实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var counter int

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                mu.Lock()
                counter++
                mu.Unlock()
            }
        }()
    }

    wg.Wait()
    fmt.Println("Counter (with lock):", counter)
}

2.原子操作

接下来,我们来看如何使用原子操作来实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
    var wg sync.WaitGroup
    var counter int64

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for j := 0; j < 1000; j++ {
                atomic.AddInt64(&counter, 1)
            }
        }()
    }

    wg.Wait()
    fmt.Println("Counter (with atomic):", counter)
}

这两种方式都可以实现多个goroutine对同一个变量进行累加操作,但是使用原子操作会更加高效,因为它不需要加锁,而是通过硬件原语来确保操作的原子性。

载入操作

在 Go 语言的 sync/atomic 包中,载入操作指的是用于原子加载变量值的函数。这些函数可以确保在并发环境中安全地获取变量的当前值,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic 包中的载入操作:

  1. Load 函数系列

    • LoadInt32LoadInt64LoadUint32LoadUint64LoadUintptr:这些函数用于加载对应类型的变量的值,并返回当前值。函数原型如下:
      1
      2
      3
      4
      5
      
      func LoadInt32(addr *int32) (val int32)
      func LoadInt64(addr *int64) (val int64)
      func LoadUint32(addr *uint32) (val uint32)
      func LoadUint64(addr *uint64) (val uint64)
      func LoadUintptr(addr *uintptr) (val uintptr)
  2. 载入操作注意事项

    • 这些载入操作都是原子的,即在执行过程中不会被中断。
    • 这些函数都接受指向对应类型的变量的指针作为参数。
    • 这些函数会返回当前变量的值,因此可以用于获取变量的当前状态。
    • 这些函数仅仅是读取变量的值,并不会修改变量的内容。
  3. 示例代码

    1
    2
    3
    4
    5
    6
    7
    
    func main() {
        var count int32 = 10
    
        // 载入操作
        val := atomic.LoadInt32(&count)
        fmt.Println("Current value:", val) // 输出: Current value: 10
    }

比较并交换

在 Go 语言的 sync/atomic 包中,比较并交换(CAS,Compare-And-Swap)操作是一种常见的并发原子操作。CAS 操作用于在并发环境中安全地更新变量的值,其原理是**先比较变量的当前值和预期值,如果相等,则将变量的新值写入;如果不相等,则不做任何操作。**CAS 操作能够避免竞态条件和数据竞争问题。下面详细介绍了 sync/atomic 包中的比较并交换操作:

  1. CompareAndSwap 函数系列

    • CompareAndSwapInt32CompareAndSwapInt64CompareAndSwapUint32CompareAndSwapUint64CompareAndSwapUintptr:这些函数用于对对应类型的变量进行比较并交换操作。函数原型如下:
      1
      2
      3
      4
      5
      
      func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
      func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
      func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
      func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
      func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
  2. 比较并交换操作注意事项

    • 这些比较并交换操作都是原子的,即在执行过程中不会被中断。
    • 这些函数都接受指向对应类型的变量的指针作为第一个参数。
    • 参数 old 是预期值,参数 new 是要更新的新值。
    • 如果变量的当前值等于预期值 old,则将新值 new 写入,并返回 true;否则不做任何操作,并返回 false
    • 这些函数通常用于实现自旋锁、无锁数据结构等并发算法中。
  3. 示例代码

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    func main() {
        var count int32 = 10
    
        // 比较并交换操作
        swapped := atomic.CompareAndSwapInt32(&count, 10, 20)
        fmt.Println("Swapped:", swapped) // 输出: Swapped: true
        fmt.Println("New value:", count) // 输出: New value: 20
    
        swapped = atomic.CompareAndSwapInt32(&count, 15, 30)
        fmt.Println("Swapped:", swapped) // 输出: Swapped: false
        fmt.Println("New value:", count) // 输出: New value: 20 (未更新)
    }

交换

在 Go 语言的 sync/atomic 包中,交换操作是一种原子操作,用于在并发环境中安全地交换变量的值。交换操作可以将变量的新值存储到指定变量中,并返回原来的值,同时保证操作的原子性,避免了竞态条件和数据竞争问题。下面详细介绍了 sync/atomic 包中的交换操作:

  1. Swap 函数系列

    • SwapInt32SwapInt64SwapUint32SwapUint64SwapUintptr:这些函数用于对对应类型的变量进行交换操作。函数原型如下:

      1
      2
      3
      4
      5
      
      func SwapInt32(addr *int32, new int32) (old int32)
      func SwapInt64(addr *int64, new int64) (old int64)
      func SwapUint32(addr *uint32, new uint32) (old uint32)
      func SwapUint64(addr *uint64, new uint64) (old uint64)
      func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
  2. 交换操作注意事项

    • 这些交换操作都是原子的,即在执行过程中不会被中断。
    • 这些函数都接受指向对应类型的变量的指针作为第一个参数。
    • 参数 new 是要存储到变量中的新值。
    • 这些函数会将变量的新值存储到指定变量中,并返回原来的值。
    • 这些函数常用于更新变量值的同时获取变量的旧值。
  3. 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    
    func main() {
        var count int32 = 10
    
        // 交换操作
        old := atomic.SwapInt32(&count, 20)
        fmt.Println("Old value:", old) // 输出: Old value: 10
        fmt.Println("New value:", count) // 输出: New value: 20
    }

存储

在 Go 语言的 sync/atomic 包中,存储操作用于将新值存储到变量中,并确保该操作是原子的,即在执行过程中不会被中断,以避免竞态条件和数据竞争问题。存储操作是一种常见的原子操作,能够帮助开发者在并发环境中安全地更新变量的值。下面详细介绍了 sync/atomic 包中的存储操作:

  1. Store 函数系列

    • StoreInt32StoreInt64StoreUint32StoreUint64StoreUintptr:这些函数用于将对应类型的新值存储到指定变量中。函数原型如下:

      1
      2
      3
      4
      5
      
      func StoreInt32(addr *int32, val int32)
      func StoreInt64(addr *int64, val int64)
      func StoreUint32(addr *uint32, val uint32)
      func StoreUint64(addr *uint64, val uint64)
      func StoreUintptr(addr *uintptr, val uintptr)
  2. 存储操作注意事项

    • 这些存储操作都是原子的,即在执行过程中不会被中断。
    • 这些函数都接受指向对应类型的变量的指针作为第一个参数。
    • 参数 val 是要存储到变量中的新值。
    • 这些函数会将新值存储到指定变量中,覆盖掉原来的值。
  3. 示例代码

    1
    2
    3
    4
    5
    6
    7
    
    func main() {
        var count int32 = 10
    
        // 存储操作
        atomic.StoreInt32(&count, 20)
        fmt.Println("New value:", count) // 输出: New value: 20
    }

StoreInt32 仅用于存储新值,不返回旧值;SwapInt32 在存储新值的同时返回旧值。

atomic.Value

atomic.Value 是 Go 语言标准库 sync/atomic 包中的一个数据结构,用于在并发环境中安全地存储和加载任意类型的值。相比于普通的变量,atomic.Value 提供了一种更为灵活和安全的方式来管理共享状态,特别适用于需要在多个 goroutine 之间传递和共享数据的情况。下面是对 atomic.Value 的详细介绍:

  1. 原子值(Atomic Value)

    • atomic.Value 是一个原子值,可以存储和加载任意类型的值,而不需要担心并发访问导致的竞态条件和数据竞争问题。
    • 它提供了 LoadStore 方法用于原子加载和存储操作,以及 Swap 方法用于原子交换操作。
  2. 零值(Zero Value)

    • atomic.Value 的零值是一个空的原子值,即初始状态下不包含任何值。
    • 零值不能通过 Load 方法加载,因为没有存储值。加载操作将会返回 nil
  3. 加载操作(Load)

    • Load 方法用于原子加载操作,返回当前存储的值。
    • 如果在调用 Load 方法之前没有调用 Store 方法存储值,那么 Load 方法将返回 nil
    • 加载操作是原子的,即在执行过程中不会被中断。
  4. 存储操作(Store)

    • Store 方法用于原子存储操作,存储指定的值到 atomic.Value 中。
    • 存储操作是原子的,即在执行过程中不会被中断。
    • 存储操作将替换原有的值,因此后续加载操作将返回最新存储的值。
  5. 交换操作(Swap)

    • Swap 方法用于原子交换操作,将新值存储到 atomic.Value 中,并返回原来的旧值。
    • 交换操作是原子的,即在执行过程中不会被中断。
    • 这个操作可以用于更新变量的值的同时获取变量的旧值。
  6. 适用场景

    • atomic.Value 适用于需要在多个 goroutine 之间安全地传递和共享数据的场景。
    • 它特别适用于需要频繁地更新共享状态的情况,而且不需要使用复杂的同步机制来保证并发安全性。
  7. 注意事项

    • 虽然 atomic.Value 提供了原子操作的支持,但是它并不是万能的。在某些情况下,可能需要考虑更高级的同步机制来保证数据的一致性和正确性。
    • 在使用 atomic.Value 时,需要仔细考虑并发访问的场景和操作序列,以确保数据的一致性和正确性。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    // 创建一个 atomic.Value,用于存储整数值
    var sharedValue atomic.Value

    // 使用 sync.WaitGroup 来等待所有 goroutine 完成
    var wg sync.WaitGroup
    wg.Add(2)

    // 启动第一个 goroutine,向共享值中存储数据
    go func() {
        defer wg.Done()

        // 存储整数值 42 到共享值中
        sharedValue.Store(42)
        fmt.Println("Goroutine 1: Stored value 42")

        // 模拟一些处理时间
        // 在实际应用中,这里可能是一些耗时的操作
        // 例如计算、I/O 操作等
        // 这些操作不会影响共享值的存储和加载
        for i := 0; i < 1e6; i++ {
            // 模拟一些处理时间
        }
    }()

    // 启动第二个 goroutine,从共享值中加载数据
    go func() {
        defer wg.Done()

        // 加载共享值中的数据
        value := sharedValue.Load()
        fmt.Println("Goroutine 2: Loaded value:", value)

        // 模拟一些处理时间
        // 在实际应用中,这里可能是一些耗时的操作
        // 例如计算、I/O 操作等
        // 这些操作不会影响共享值的存储和加载
        for i := 0; i < 1e6; i++ {
            // 模拟一些处理时间
        }
    }()

    // 等待所有 goroutine 完成
    wg.Wait()
    fmt.Println("All goroutines have completed")
}

在这个示例中,我们首先创建了一个 atomic.Value,用于存储整数值。然后启动了两个 goroutine,在第一个 goroutine 中存储了整数值 42 到共享值中,在第二个 goroutine 中加载了共享值中的数据。由于 atomic.Value 提供了原子操作,因此无需额外的锁机制来保证并发安全性,两个 goroutine 可以安全地在不同的时间点对共享值进行存取操作。

互斥锁与原子操作

在并发编程里,Go语言sync包里的同步原语Mutex是我们经常用来保证并发安全的,但是他跟atomic包在使用目的和底层实现上都不一样。

使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。

底层实现Mutex由操作系统的调度器实现,而atomic包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。

atomic的底层实现参考:Golang 源码分析系列之 atomic 底层实现

对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势。

0%