首页 > 解决方案 > atomic.Load 和 atomic.Store 的意义何在

问题描述

在 Go 的内存模型中,没有任何关于原子及其与内存围栏的关系的说明。

尽管许多内部包似乎依赖于可以提供的内存排序,如果原子在它们周围创建内存栅栏。有关详细信息,请参阅此问题

在不了解它的真正工作原理之后,我去了源代码,特别是src/runtime/internal/atomic/atomic_amd64.go并找到了以下Loadand的实现Store

//go:nosplit
//go:noinline
func Load(ptr *uint32) uint32 {
    return *ptr
}

Storeasm_amd64.s在同一个包中实现。

TEXT runtime∕internal∕atomic·Store(SB), NOSPLIT, $0-12
    MOVQ    ptr+0(FP), BX
    MOVL    val+8(FP), AX
    XCHGL   AX, 0(BX)
    RET

两者看起来都与并行性无关。

我确实研究了其他架构,但实现似乎是等效的。

但是,如果原子确实很弱并且没有提供内存排序保证,那么下面的代码可能会失败,但它不会。

作为补充,我尝试用简单的赋值替换原子调用,但在这两种情况下它仍然产生一致且“成功”的结果。


func try() {
    var a, b int32

    go func() {
        // atomic.StoreInt32(&a, 1)
        // atomic.StoreInt32(&b, 1)
        a = 1
        b = 1
    }()

    for {
        // if n := atomic.LoadInt32(&b); n == 1 {
        if n := b; n == 1 {
            if a != 1 {
                panic("fail")
            }
            break
        }
        runtime.Gosched()
    }
}

func main() {
    n := 1000000000
    for i := 0; i < n ; i++ {
        try()
    }
}

下一个想法是编译器做了一些魔术来提供排序保证。因此,下面是具有原子且未注释的变体Store列表Load。完整列表可在pastebin上找到。

// Anonymous function implementation with atomic calls inlined

TEXT %22%22.try.func1(SB) gofile../path/atomic.go
        atomic.StoreInt32(&a, 1)
  0x816         b801000000      MOVL $0x1, AX
  0x81b         488b4c2408      MOVQ 0x8(SP), CX
  0x820         8701            XCHGL AX, 0(CX)
        atomic.StoreInt32(&b, 1)
  0x822         b801000000      MOVL $0x1, AX
  0x827         488b4c2410      MOVQ 0x10(SP), CX
  0x82c         8701            XCHGL AX, 0(CX)
    }()
  0x82e         c3          RET
// Important "cycle" part of try() function

 0x6ca          e800000000      CALL 0x6cf      [1:5]R_CALL:runtime.newproc
    for {
  0x6cf         eb12            JMP 0x6e3
        runtime.Gosched()
  0x6d1         90          NOPL
    checkTimeouts()
  0x6d2         90          NOPL
    mcall(gosched_m)
  0x6d3         488d0500000000      LEAQ 0(IP), AX      [3:7]R_PCREL:runtime.gosched_m·f
  0x6da         48890424        MOVQ AX, 0(SP)
  0x6de         e800000000      CALL 0x6e3      [1:5]R_CALL:runtime.mcall
        if n := atomic.LoadInt32(&b); n == 1 {
  0x6e3         488b442420      MOVQ 0x20(SP), AX
  0x6e8         8b08            MOVL 0(AX), CX
  0x6ea         83f901          CMPL $0x1, CX
  0x6ed         75e2            JNE 0x6d1
            if a != 1 {
  0x6ef         488b442428      MOVQ 0x28(SP), AX
  0x6f4         833801          CMPL $0x1, 0(AX)
  0x6f7         750a            JNE 0x703
  0x6f9         488b6c2430      MOVQ 0x30(SP), BP
  0x6fe         4883c438        ADDQ $0x38, SP
  0x702         c3          RET

如您所见,没有栅栏或锁再次到位。

注意:所有测试均在 x86_64 和 i5-8259U 上完成

问题:

那么,在函数调用中包装简单的指针取消引用是否有任何意义,或者它是否有一些隐藏的含义,为什么这些原子仍然作为内存屏障工作?(如果他们这样做)

标签: multithreadinggoassemblyparallel-processingatomic

解决方案


我根本不知道 Go,但它看起来像 x86-64 实现.load()并且.store() 顺序一致的。 大概是故意/出于某种原因!

//go:noinline我认为,加载意味着编译器无法围绕黑盒非内联函数重新排序。在 x86 上,这就是顺序一致性或 acq-rel 的负载方面所需的全部内容。普通的 x86mov负载是获取负载。

编译器生成的代码可以利用x86 的强排序内存模型,即顺序一致性 + 存储缓冲区(带有存储转发),即 acq/rel。 要恢复顺序一致性,您只需要在释放存储之后清空存储缓冲区。

.store()用 asm 编写,加载其堆栈参数并xchg用作 seq-cst 存储。


XCHGwith memory 有一个隐式lock前缀,它是一个完整的屏障;mov它是+的有效替代方案,mfence可以实现 C++ 所称的memory_order_seq_cst商店。

它在以后的加载和存储允许触及 L1d 缓存之前刷新存储缓冲区。 为什么具有顺序一致性的 std::atomic 存储使用 XCHG?


推荐阅读