请教一个竞争问题

rbaloatiw · 2024-9-5 22:32:06 · 111 次点击
[go memory model]( https://go.dev/ref/mem) 中说:
> ...each read of a single-word-sized or sub-word-sized memory location must observe a value actually written to that location (perhaps by a concurrent executing goroutine) and not yet oferwritten.

这句话是否可以理解为读一个字长以下的数据, 总是会读到某一次写入的数据, 而不会读到某个中间状态?

如果上述理解是正确的, 那么对于下面的程序:
```go
package main

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

type A struct {
        data string
}

func main() {
        a := &A{data: "b"}
   
        go func() {
                for {
                        if a.data == "a" {
                                a = &A{data: "b"}
                        } else {
                                a = &A{data: "a"}
                        }
                }
        }()

        var wg sync.WaitGroup
        for i := 0; i < 100; i++ {
                wg.Add(1)
                go func() {
                        for i := 0; i < 100000; i++ {
                                // 复制 a 的指针, aa 在接下来的使用中应该指向同一个 A
                                aa := a
                                if aa.data != "a" && aa.data != "b" {
                                        panic(aa.data)
                                }
                        }
                        wg.Done()
                }()
        }

        start := time.Now()
        wg.Wait()
        fmt.Println(time.Since(start))
}
```
由于指针 `*A` 是一个字长, 那么读取变量 a 总是会读到某一个 A 地址, 所以 panic 不会发生, 但实际上会出现:
```bash
panic:

goroutine 6 [running]:
main.main.func2()
        /Users/a/test/test.go:44 +0xa0
created by main.main in goroutine 1
        /Users/a/test/test.go:40 +0x44
exit status 2
```
这是为什么?
举报· 111 次点击
登录 注册 站外分享
9 条回复  
zhouyin 小成 2024-9-5 23:06:12
string 底层表示不是一个 byte  不是原子操作 换成 byte 试试
kk2syc 初学 2024-9-5 23:08:12
没 panic 啊
https://i.imgur.com/2QXOgV5.png
iceheart 小成 2024-9-5 23:16:21
sizeof A = 16
Trim21 小成 2024-9-5 23:24:32
并没有 panic

顺便前面#1 和#3 理解错了,这里操作的 a 是个*A ,跟 string 和 A 的大小没关系。
nagisaushio 小成 2024-9-5 23:59:16
Intel ,同没有 panic 。

建议研究一下生成的汇编代码,看看具体是怎么运行的。
Orlion 小成 2024-9-6 11:00:01
首先从理论上来说,`aa.data != "a" && aa.data != "b"` 这一行代码不是原子的,有可能出现这种情况:
在判断 aa.data != "a"时,aa.data="b"
随后在判断 aa.data != "b"时,aa.data 被修改为了"a"

这种情况下是可能触发 panic 的


然而这不是唯一的原因,因为你的代码 panic 出来的信息 aa.data 是空,因此还有其他方面的原因
zizon 小成 2024-9-6 12:32:19
Panic 堆栈的代码行数和你这个对不上吧?
MoYi123 小成 2024-9-6 18:00:09

请教一个竞争问题

先把 A{data: "a"}和 A{data: "b"}构造好, 循环里直接换它们的指针就不会有错,
我猜测顺序是 alloc 内存 -> 更新指针 -> 给 string 赋值, 所以出现了不是 a 或 b 的情况.
oaix 小成 2024-9-7 01:02:59

请教一个竞争问题

CPU 乱序执行。

> 在 x86-64 (x64) 和 ARM64 (AArch64) 处理器架构中,乱序执行( Out-of-Order Execution )是用于提高处理器性能的一种技术。两种架构在乱序执行和内存模型方面有所不同,其中 ARM64 的内存模型通常被认为比 x86-64 更加“激进”或更弱。

x86-64 和 ARM64 的内存模型对比
x86-64 (x64) 内存模型:

强内存模型:x86-64 处理器通常有一个较为强的一致性内存模型。这意味着大多数内存操作(特别是读写操作)的顺序与程序中的顺序是一致的。写入操作一般不能在读取操作之前发生,也不能跨越其他写入操作。这种强内存模型使得编写并发代码相对容易。
乱序执行限制:虽然 x86-64 处理器执行乱序执行,但它在内存操作的乱序方面受到限制。处理器会自动维护内存操作的一些顺序,特别是写-读依赖关系,不需要开发者过多使用内存屏障。
ARM64 (AArch64) 内存模型:

弱内存模型:与 x86-64 相比,ARM64 使用了更弱的内存模型。这意味着处理器可以以更加激进的方式重新排序内存操作。比如,写入操作可以跨越读取操作,甚至不同线程的内存操作顺序可能会被打乱,这在多线程编程中可能导致不可预期的结果。
乱序执行更激进:ARM64 的乱序执行在内存操作上更为激进,需要更多地依赖于显式的内存屏障来确保内存操作的顺序。这使得 ARM64 的性能可能更高,但也增加了并发编程的复杂性。开发者必须通过 dmb 、dsb 等指令或使用内存屏障来控制内存操作的顺序。
总结
x86-64 的内存模型更强,乱序执行更保守:在大多数情况下,x86-64 处理器会确保内存操作顺序与程序代码顺序大致一致,使得并发编程相对简单。
ARM64 的内存模型更弱,乱序执行更激进:ARM64 处理器允许更多的内存操作乱序执行,因此在并发编程中需要更加注意内存屏障的使用,以避免数据一致性问题。
因此,ARM64 的乱序执行比 x86-64 更加激进,也更依赖于显式的同步操作来确保内存操作的正确性。
返回顶部