深入理解Golang的atomic原子操作

2023-06-02 11:23:29 浏览数 (2)

Golang的atomic包提供了一组原子操作函数,用于在多个goroutine之间安全地访问和修改共享变量。这些原子操作函数可以保证对共享变量的操作原子性的,从而避免了竞态条件的发生。本文将深入探讨Golang的atomic包的原子操作。

原子操作的实现原理

Golang的atomic包的原子操作是通过CPU指令实现的。在大多数CPU架构中,原子操作的实现都是基于32位64位的寄存器。Golang的atomic包的原子操作函数会将变量的地址转换为指针型的变量,并使用CPU指令对这个指针型的变量进行操作。

例如,当我们调用AddInt32函数时,Golang会将变量的地址转换为int32类型的指针,并使用CPU提供的原子指令对这个指针型的变量进行加法操作。这样,就可以保证对共享变量的操作是原子性的。

需要注意的是,不同的CPU架构可能会提供不同的原子指令。因此,在使用atomic包的原子操作时,需要根据具体的CPU架构来选择合适的原子操作函数。

汇编过程

在x86架构的CPU上,原子操作是通过lock指令实现的。lock指令可以将内存操作变成原子操作,保证多个CPU同时访问同一内存地址时的正确性。例如,下面是一个在x86架构上实现的AddInt32函数的汇编代码:

代码语言:text复制
TEXT ·AddInt32(SB), NOSPLIT, $0-12
    MOVQ ptr 0(FP), AX
    MOVQ old 8(FP), BX
    MOVQ new 0(FP), CX
    LOCK
    XADDL CX, (AX)
    CMP CX, BX
    JNE fail
    MOVQ $1, AX
    RET
fail:
    MOVQ $0, AX
    RET

在上面的代码中,我们定义了一个AddInt32函数,它接受三个参数:ptroldnew。这些参数分别表示要操作的内存地址、旧值和新值。在函数的实现中,我们使用了lock指令XADDL指令变成了原子操作,保证了多个CPU同时访问同一内存地址时的正确性。

原子操作函数

Golang的atomic包提供了一组原子操作函数,包括AddCompareAndSwapLoadStoreSwap等函数。这些函数的具体作用如下:

  • Add函数:用于对一个整数型的变量进行加法操作,并返回新的值。
  • CompareAndSwap函数:用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。
  • Load函数:用于获取一个指针型的变量的值。
  • Store函数:用于设置一个指针型的变量的值。
  • Swap函数:用于交换一个指针型的变量的值,并返回旧值。

让我们更具体地来看一下Golang的atomic包的原子操作:

1. Add函数

Add函数用于对一个整数型的变量进行加法操作,并返回新的值。Add函数的定义如下:

代码语言:go复制
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)

其中,addr表示要进行加法操作的变量的地址,delta表示要加上的值。Add函数会将变量的值加上delta,并返回新的值。

2. CompareAndSwap函数

CompareAndSwap函数用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。CompareAndSwap函数的定义如下:

代码语言:go复制
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)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

其中,addr表示要进行比较和交换的变量的地址,old表示旧值,new表示新值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。

3. Load函数

Load函数用于获取一个指针型的变量的值。Load函数的定义如下:

代码语言:go复制
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)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

其中,addr表示要获取的变量的地址。Load函数会返回变量的值。

4. Store函数

Store函数用于设置一个指针型的变量的值。Store函数的定义如下:

代码语言:go复制
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)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

其中,addr表示要设置的变量的地址,val表示要设置的值。Store函数会将变量的值设置为val。

5. Swap函数

Swap函数用于交换一个指针型的变量的值,并返回旧值。Swap函数的定义如下:

代码语言:go复制
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)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

其中,addr表示要交换的变量的地址,new表示新值。Swap函数会将变量的值设置为new,并返回旧值

原子操作的使用示例

1. Add函数示例

Add函数用于对一个整数型的变量进行加法操作,并返回新的值。下面是一个Add函数的示例代码:

代码语言:go复制
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0
    for i := 0; i < 100; i   {
        go func() {
            atomic.AddInt32(&count, 1)
        }()
    }
    for atomic.LoadInt32(&count) < 100 {
    }
    fmt.Println("count:", count)
}

在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了100个goroutine,每个goroutine都会对count变量进行加1操作。在主goroutine中,我们使用atomic.LoadInt32函数来获取count变量的值,如果count变量的值小于100,就继续等待。当count变量的值等于100时,我们打印count变量的值。

2. CompareAndSwap函数示例

CompareAndSwap函数用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。下面是一个CompareAndSwap函数的示例代码:

代码语言:go复制
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0
    for i := 0; i < 100; i   {
        go func() {
            for {
                old := atomic.LoadInt32(&count)
                new := old   1
                if atomic.CompareAndSwapInt32(&count, old, new) {
                    break
                }
            }
        }()
    }
    for atomic.LoadInt32(&count) < 100 {
    }
    fmt.Println("count:", count)
}

在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了100个goroutine,每个goroutine都会对count变量进行加1操作。在每个goroutine中,我们使用for循环来进行比较和交换操作,直到成功为止。在主goroutine中,我们使用atomic.LoadInt32函数来获取count变量的值,如果count变量的值小于100,就继续等待。当count变量的值等于100时,我们打印count变量的值。

3. Load函数示例

Load函数用于获取一个指针型的变量的值。下面是一个Load函数的示例代码:

代码语言:go复制
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0
    go func() {
        for {
            fmt.Println("count:", atomic.LoadInt32(&count))
        }
    }()
    for i := 0; i < 100; i   {
        atomic.AddInt32(&count, 1)
    }
    fmt.Println("count:", count)
}

=======
结果:
count: 100

在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了一个goroutine,用于不断地获取count变量的值。在主goroutine中,我们使用atomic.AddInt32函数对count变量进行加1操作。由于Load函数是非阻塞的,因此我们可以在另一个goroutine中使用Load函数来获取count变量的值。

4. Store函数示例

Store函数用于设置一个指针型的变量的值。下面是一个Store函数的示例代码:

代码语言:go复制
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0
    atomic.StoreInt32(&count, 100)
    fmt.Println("count:", count)
}

=======
结果:
count: 100

在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们使用atomic.StoreInt32函数将count变量的值设置为100。最后,我们打印count变量的值。

5. Swap函数示例

Swap函数用于交换一个指针型的变量的值,并返回旧值。下面是一个Swap函数的示例代码:

代码语言:go复制
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var count int32 = 0
    old := atomic.SwapInt32(&count, 100)
    fmt.Println("old:", old)
    fmt.Println("count:", count)
}

=======
结果:
old: 0
count: 100

在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们使用atomic.SwapInt32函数将count变量的值设置为100,并将旧值保存到old变量中。最后,我们打印旧值和新值。

需要注意的是,使用atomic包的原子操作时,需要保证对共享变量的操作都是原子性的。如果在原子操作之外对共享变量进行了操作,就可能会导致竞态条件的发生。因此,在使用atomic包的原子操作时,需要仔细考虑代码的逻辑和数据的共享方式。

使用atomic需要注意

  1. 原子操作只能对共享变量进行操作,不能对私有变量进行操作。
  2. 原子操作只能对基本类型的变量进行操作,不能对复杂类型的变量进行操作。
  3. 原子操作不能保证程序的正确性,只能保证程序的原子性。因此,在使用原子操作时,需要仔细考虑代码的逻辑和数据的共享方式。
  4. 原子操作的性能比普通操作要低,因此,在不必要的情况下,应该尽量避免使用原子操作。
  5. 在使用原子操作时,需要保证对共享变量的操作都是原子性的。如果在原子操作之外对共享变量进行了操作,就可能会导致竞态条件的发生。
  6. 在使用原子操作时,需要注意内存模型的影响。不同的内存模型对原子操作的行为有不同的规定,因此,在使用原子操作时,需要仔细阅读相关文档,了解内存模型的规定。
  7. 在使用原子操作时,需要注意数据的对齐方式。如果数据没有按照正确的对齐方式进行存储,就可能会导致原子操作的失败。

总之,在使用atomic包时,需要仔细考虑代码的逻辑和数据的共享方式,避免出现竞态条件和内存模型的问题。同时,需要注意原子操作的性能和数据的对齐方式,以提高程序的效率和正确性。

疑问?

1. 为什么原子操作只能是AddInt32或者AddInt64,而没有AddInt16AddInt128呢?

在大多数平台上,CPU并不支持原子操作16位整数。在这些平台上,对16位整数进行原子操作需要使用32位整数或64位整数来实现。因此,在Go语言的atomic包中,没有提供AddInt16函数

2. 为什么CPU并不支持原子操作16位整数?

在早期的CPU架构中,16位整数并不是主流的数据类型,因此CPU并没有专门为16位整数提供原子操作的支持。相反,CPU更加关注32位整数和64位整数的原子操作,因为这些数据类型更加常见和重要。

此外,原子操作需要保证多个CPU同时访问同一内存地址时的正确性,因此需要使用锁机制来实现。锁机制会增加CPU的开销和复杂度,因此CPU需要权衡性能和功能的考虑。在这种情况下,CPU更倾向于支持更常见和重要的数据类型,而不是支持所有可能的数据类型。

3. 使用原子操作,如何保证业务逻辑正确性?

使用原子操作可以保证多个线程或进程同时访问同一内存地址时的正确性,但并不能保证业务逻辑的正确性。因此,在使用原子操作时,需要注意以下几点,以确保业务逻辑的正确性:

  • 原子操作的粒度:原子操作应该尽可能小,只包含必要的操作。如果原子操作的粒度过大,可能会导致锁的竞争过于激烈,从而影响程序的性能。
  • 原子操作的错误处理:原子操作应该正确处理错误情况。如果原子操作的错误处理不正确,可能会导致数据的不一致性。

4. 如何保证原子操作成功?

对原子操作返回的结果进行判断处理,至少需要有失败重试机制。

特别注意

原子操作,只能保证操作的原子性,但是不能保证操作一定成功。

0 人点赞