multithreading - atomic.Load 和atomic.Store 的意义是什么

标签 multithreading go assembly parallel-processing atomic

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

尽管许多内部包似乎依赖于原子在它们周围创建内存栅栏时可以提供的内存排序。请参阅this issue了解详情。

在不明白它是如何工作的之后,我查阅了资料来源,特别是 src/runtime/internal/atomic/atomic_amd64.go并发现以下 LoadStore 的实现:

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

Store 在同一包中的 asm_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()
    }
}

接下来的想法是编译器做了一些魔法来提供排序保证。因此,下面是未注释原子 StoreLoad 变体的列表。完整列表可在 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 上完成

问题:

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

最佳答案

我根本不懂 Go,但它看起来像是 .load().store() 的 x86-64 实现 顺序一致。大概是有目的/有原因的!

我认为,加载上的

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

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

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

<小时/> 带有内存的

XCHG 有一个隐式的 lock 前缀,它是一个完整的屏障;它是 mov+mfence 的有效替代方案,用于实现 C++ 所称的 memory_order_seq_cst 存储。

它会在以后的加载和存储访问 L1d 缓存之前刷新存储缓冲区。 <强> Why does a std::atomic store with sequential consistency use XCHG?

参见

关于multithreading - atomic.Load 和atomic.Store 的意义是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58581820/

相关文章:

go - "go run"使用 sublime text 3 进入无限循环

go - 从文件和存储文件创建 sha256 的最佳模式

c - 从 C 测试 Shellcode - 总线错误 10

java - 理解 concurrentHashMap

multithreading - 如何将对堆栈变量的引用传递给线程?

c - 如何将 uint8_t 数组从 C 发送到 GO

assembly - 在编译器中实现闭包

optimization - 加速 x64 汇编器 ADD 循环

Java 如何线程化 GUI

c# - 强制 .NET、多线程 volatile 优化错误