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
这是为什么?
1
zhouyin 77 天前 via Android
string 底层表示不是一个 byte 不是原子操作 换成 byte 试试
|
2
kk2syc 77 天前
|
3
iceheart 77 天前 via Android
sizeof A = 16
|
4
Trim21 77 天前
并没有 panic
顺便前面#1 和#3 理解错了,这里操作的 a 是个*A ,跟 string 和 A 的大小没关系。 |
5
Trim21 77 天前
我理解的跟你一样,这种情况下虽然 go 的 race 检查会报错但是实际上是安全的
|
6
rbaloatiw OP |
7
nagisaushio 77 天前
Intel ,同没有 panic 。
建议研究一下生成的汇编代码,看看具体是怎么运行的。 |
8
Orlion 76 天前
首先从理论上来说,`aa.data != "a" && aa.data != "b"` 这一行代码不是原子的,有可能出现这种情况:
在判断 aa.data != "a"时,aa.data="b" 随后在判断 aa.data != "b"时,aa.data 被修改为了"a" 这种情况下是可能触发 panic 的 然而这不是唯一的原因,因为你的代码 panic 出来的信息 aa.data 是空,因此还有其他方面的原因 |
9
rbaloatiw OP @Orlion #8 我并没有修改 a.data 的操作, 在写入线程中都是新建一个结构体赋值给 a, 而下面 `aa := a` 复制了指针 a, 这时候即使 a 被赋了新值 aa 也不会改变. 所以应该不会出现在判断 `aa.data != "a" && aa.data != "b"` 时 aa 指向的结构体变化了的情况.
|
10
zizon 76 天前
Panic 堆栈的代码行数和你这个对不上吧?
|
12
MoYi123 76 天前
先把 A{data: "a"}和 A{data: "b"}构造好, 循环里直接换它们的指针就不会有错,
我猜测顺序是 alloc 内存 -> 更新指针 -> 给 string 赋值, 所以出现了不是 a 或 b 的情况. |
13
oaix 76 天前 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 更加激进,也更依赖于显式的同步操作来确保内存操作的正确性。 |
14
rbaloatiw OP #13 应该是对的. 这个例子应该非常类似 go memory model 中"不正确的同步"一节中的例子. 更详细的解释可以看 rsc 的 [Hardware Memory Models]( https://research.swtch.com/hwmm) 这篇博客.
一个简单的解决办法是把 `a` 换成 `atomic.Value` 来进行同步. |
15
kingcanfish 73 天前
a := &A{data: "b"} 这条语句其实是两个动作
一个是初始化 A 之后在复制给 a (此时 data 已经有值); 另一种是先初始化了个空的 A 地址,赋值给 a, 然后再给 data 赋值; 第二种情况就会发生 panic (在 data 赋值之前,另一个协程就已经对 data 的值进行检查了) 这两种情况和架构上的指令重排应该有关系,arm 内存模型比 amd 宽松 所以理论上遇到的概率更大 |