请教一个竞争问题 - V2EX
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
rbaloatiw

请教一个竞争问题

  •  
  •   rbaloatiw Sep 5, 2024 2642 views
    This topic created in 632 days ago, the information mentioned may be changed or developed.

    go memory model 中说:

    ...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 overwritten.

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

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

    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 不会发生, 但实际上会出现:

    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 

    这是为什么?

    15 replies    2024-09-09 18:55:27 +08:00
    7uSK0CV63kJdhp0M
        1
    7uSK0CV63kJdhp0M  
       Sep 5, 2024 via Android
    string 底层表示不是一个 byte 不是原子操作 换成 byte 试试
    v1
        2
    v1  
       Sep 5, 2024
    没 panic 啊
    iceheart
        3
    iceheart  
       Sep 5, 2024 via Android
    sizeof A = 16
    Trim21
        4
    Trim21  
       Sep 5, 2024
    并没有 panic

    顺便前面#1 和#3 理解错了,这里操作的 a 是个*A ,跟 string 和 A 的大小没关系。
    Trim21
        5
    Trim21  
       Sep 5, 2024
    我理解的跟你一样,这种情况下虽然 go 的 race 检查会报错但是实际上是安全的
    rbaloatiw
        6
    rbaloatiw  
    OP
       Sep 5, 2024
    @zhouyin
    @iceheart 我的理解和 #4 是一样的, 操作的 a 是个指针, 所以大小为 8B (64 位机器)

    @Trim21 #5 @kk2syc #2 主楼是在 apple silicon arm 下跑的, 我又去 intel x86 下跑了一下, 试了好几次也都没有 panic, 莫非和架构有关..
    nagisaushio
        7
    nagisaushio  
       Sep 5, 2024
    Intel ,同没有 panic 。

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

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


    然而这不是唯一的原因,因为你的代码 panic 出来的信息 aa.data 是空,因此还有其他方面的原因
    rbaloatiw
        9
    rbaloatiw  
    OP
       Sep 6, 2024
    @Orlion #8 我并没有修改 a.data 的操作, 在写入线程中都是新建一个结构体赋值给 a, 而下面 `aa := a` 复制了指针 a, 这时候即使 a 被赋了新值 aa 也不会改变. 所以应该不会出现在判断 `aa.data != "a" && aa.data != "b"` 时 aa 指向的结构体变化了的情况.
    zizon
        10
    zizon  
       Sep 6, 2024
    Panic 堆栈的代码行数和你这个对不上吧?
    Orlion
        11
    Orlion  
       Sep 6, 2024
    @rbaloatiw 确实,是我草率了
    MoYi123
        12
    MoYi123  
       Sep 6, 2024
    先把 A{data: "a"}和 A{data: "b"}构造好, 循环里直接换它们的指针就不会有错,
    我猜测顺序是 alloc 内存 -> 更新指针 -> 给 string 赋值, 所以出现了不是 a 或 b 的情况.
    oaix
       
    oaix  
       Sep 7, 2024   1
    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 更加激进,也更依赖于显式的同步操作来确保内存操作的正确性。
    rbaloatiw
        14
    rbaloatiw  
    OP
       Sep 7, 2024
    #13 应该是对的. 这个例子应该非常类似 go memory model 中"不正确的同步"一节中的例子. 更详细的解释可以看 rsc 的 [Hardware Memory Models]( https://research.swtch.com/hwmm) 这篇博客.

    一个简单的解决办法是把 `a` 换成 `atomic.Value` 来进行同步.
    kingcanfish
        15
    kingcanfish  
       Sep 9, 2024
    a := &A{data: "b"} 这条语句其实是两个动作
    一个是初始化 A 之后在复制给 a (此时 data 已经有值);
    另一种是先初始化了个空的 A 地址,赋值给 a, 然后再给 data 赋值;
    第二种情况就会发生 panic (在 data 赋值之前,另一个协程就已经对 data 的值进行检查了)
    这两种情况和架构上的指令重排应该有关系,arm 内存模型比 amd 宽松 所以理论上遇到的概率更大
    About     Help     Advertise     Blog     API     FAQ     Solana     1667 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 50ms UTC 16:19 PVG 00:19 LAX 09:19 JFK 12:19
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86