二话不说,上demo
const (
FIRST = "WHAT THE"
SECOND = "F*CK"
)
func main() {
var s string
go func() {
i := 1
for {
i = 1 - i
if i == 0 {
s = FIRST
} else {
s = SECOND
}
time.Sleep(10)
}
}()
for {
if s != "WHAT THE" && s != "F*CK" && s != "" {
fmt.Println(s)
fmt.Println("--------------")
return
}
fmt.Println(s)
time.Sleep(10)
}
}
执行结果:
....
WHAT
--------------
....
F*CKGOGC
--------------
以下结果正常,if判断时,s是满足了条件的,只是打印时s正好赋值完毕,但还是进入了if
WHAT THE
--------------
那么为什么 string 的并发读写会出现这种现象呢?
这就得从 string 底层的数据结构说起了。在 go 的 reflect 包里有一个 type StringHeader ,对应的就是 string 在 go runtime的表示:
type StringHeader struct {
Data uintptr
Len int
}
可以看到, string 由一个指针(指向字符串实际内容)和一个长度组成。
比如说我们可以这么玩弄 StringHeader:
s := "hello"
p := *(*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println(p.Len)
对于这样一个 struct ,golang 无法保证原子性地完成赋值,因此可能会出现goroutine 1 刚修改完指针(Data)、还没来得及修改长度(Len),goroutine 2 就读取了这个string 的情况。
因此我们看到了 “WHAT” 这个输出 —— 这就是将 s 从 “F*CK” 改成 “WHAT THE” 时,Data 改了、Len 还没来得及改的情况(仍然等于4)。
至于 “F*CKGOGC” 则正好相反,而且显然是出现了越界,只不过越界访问的地址仍然在进程可访问的地址空间里。
越界不报错?
难道是碰巧越界访问的地址仍然在进程可访问的地址空间里?再来demo,尝试加大两个字符串之间的长度差距
const (
FIRST = "WHAT THE 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
SECOND = "F*CK"
)
func main() {
var s string
go func() {
i := 1
for {
i = 1 - i
if i == 0 {
s = FIRST
} else {
s = SECOND
}
time.Sleep(10)
}
}()
for {
if s != "WHAT THE 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" && s != "F*CK" && s != "" {
fmt.Println(s)
fmt.Println("--------------")
return
}
fmt.Println(s)
time.Sleep(10)
}
}
......
F*CKGOGCLisuMiaoModiNZDTNZSTNewaSASTThai
m=] = ] n=avx2basebindbmi1bmi2boolcallcas1cas2cas3cas4cas5cas6chandeadermsfilefuncidleint8kindpipesbrksse2sse3t
--------------
额,没报错,编译器做了优化?或例子的长度差距不够?不得而知
再用 go 的 race detector 瞅瞅:
$ go run -race poc.go >/dev/null
==================
WARNING: DATA RACE
Write at 0x00c00010a1e0 by goroutine 7:
main.main.func1()
C:/Users/user/Desktop/SEGI/src/test/10.go:20 +0x6c // s = FIRST赋值那行
Previous read at 0x00c00010a1e0 by main goroutine:
main.main()
C:/Users/user/Desktop/SEGI/src/test/10.go:29 +0x150 // if 判断那行,会做取值
Goroutine 7 (running) created at:
main.main()
C:/Users/user/Desktop/SEGI/src/test/10.go:15 +0xa7
==================
既然问题定位到了,解决起来就很简单了。最直接的方法是使用 sync.Mutex:
var mu sync.RWMutex
mu.Lock()
s = FIRST
mu.Unlock()
性能更好的方案是将 s 类型改成 atomic.Value,然后
var s atomic.Value
s.Store(FIRST)
实际上,Golang 不保证任何单独的操作是原子性的,除非使用 atomic 包里提供的原语或加锁。
总结一下:
- string 没有看起来那么人畜无害
- 并发的坑可以找 -race 帮帮忙
- 记得使用 mutex 或 atomic