直接上代码,请看图: https://pic.baixiongz.com/uploads/2021/11/30/bc91319946394.jpeg
搞不明白为什么 append slice 会 panic ,出现空指针,而且不是必现,运行一段时间才会出现,一般在几个小时内,求大佬解释一下是为什么
1
longfxxx 2021-11-30 09:41:47 +08:00 via iPhone
slice 不需要 make 一下吗?
|
2
whitehack 2021-11-30 09:42:54 +08:00 1
共享了那个 list 变量 没加锁
你先加个锁 还有问题再来问 |
3
sujin190 2021-11-30 09:43:44 +08:00
你这图和你右边的输出似乎没啥关系吧
|
4
mangoDB 2021-11-30 09:44:10 +08:00
slice 不是 thread safe 的。
|
5
sujin190 2021-11-30 09:46:28 +08:00
|
6
sadfQED2 2021-11-30 09:49:55 +08:00 via Android
@sujin190 你图片看不到。golang 里面不加锁会 panic
不过空指针应该不是加群的问题,检查下是不是并发情况导致没有初始化 |
7
driveby 2021-11-30 09:53:35 +08:00
你这不加锁不是已经 panic 了吗。应该就是 slice 没加锁的原因,照 #2 的方式多跑几遍对照一下就知道结论了。
|
9
PungentSauce 2021-11-30 09:54:54 +08:00
你这是把内存跑满了吧
|
11
sunny1688 OP @PungentSauce 内存没跑满,跑个一会就会出现,不是立马复现
@mangoDB 对,不是线程安全,最终也是 append 的数量不对,但也不应该是空指针 @longfxxx struct 会自动初始化,有零值,可以直接 append |
12
sunny1688 OP ```go
package main import ( "fmt" "sync" "time" ) type User struct { email string orders []*Order } type Order struct { no string createdAt time.Time } func main() { total := 0 for { user := &User{email: "xxxx"} wg := sync.WaitGroup{} wg.Add(6) for i := 0; i < 6; i++ { go func() { defer wg.Done() user.orders = append(user.orders, &Order{}) }() } wg.Wait() total += 1 fmt.Println(user, len(user.orders), "total=", total) time.Sleep(time.Millisecond * 200) } } ``` 这是代码,大家可以跑一段时间,然后看看会不会出现空指针 |
13
imherer 2021-11-30 10:03:47 +08:00 4
append 后 slice 如果扩容会导致 demo.list 的地址发生变化
|
14
sujin190 2021-11-30 10:07:54 +08:00
@imherer #13 然后原地址可能已经被回收,但因协程调度原因此时有协程才刚开始使用原地址进行操作这样么?嗯,极高并发下看起来还真有可能
|
17
sujin190 2021-11-30 10:18:56 +08:00
@imherer #15 但是把如果是 c 和 c++的话,原地址被回收只是代表其会被重用于其它内存分配,地址指向的物理内存是不会消失的,所以也就不会出现空指针错误,除非这是一个双重指针,地址回收的时候更新了第二层指针的指向为空
说起来实际使用来看,go 还真是这么设计的,双重指针,只是这样设计似乎效率低了一点,但是好处确实是保证不会突破内存屏障了,上层使用来看确实有些地方还是很让人莫名其妙的 |
20
sxfscool 2021-11-30 10:43:01 +08:00
先加个锁
|
21
jimmzhou 2021-11-30 10:56:11 +08:00
go run -race 跑一下 会发现 WARNING: DATA RACE
|
22
loushizan 2021-11-30 14:02:44 +08:00
@mangoDB 👍
不过准确的说,是 slice 指向的数组指针发生了变化 type SliceHeader struct { Data uintptr Len int Cap int } Data 发生了变化,slice 本身不会 |
23
Marmot 2021-11-30 14:13:55 +08:00
@imherer 这个老哥回答的才是对的,也是上面说的为什么需要加锁的原因,slice 的底层是一个数组,当触发扩容之后,会把内容 copy 到新的内存地址上面去,然后 gc 回收旧的那个,但是有些 gorountie 还在往上面写
|
24
icexin 2021-11-30 14:50:41 +08:00 5
大家回答的点都集中在内存回收上,实际的问题是没有加锁导致的不变式被打破的问题。
实际的 slice 包含 data ,len 和 cap 字段,这些大家也都知道了。slice 结构的不变式是:在任意时刻,data 指向的数据长度都是至少是 len 长度,否则访问 len-1 的数据就会 内存错误。 在题主的代码里面,多个 goroutine 同时对 demo.llist 进行赋值,但因为没有加锁,所以赋值不是原子的,从而会出现一个 goroutine 刚赋值了 data ,还没来得及赋值 data 和 cap 就被其他 goroutine 拿去用了, 破坏了不变式, 从而在扩容的时候就访问了非法内存,从而 panic 。 一段简单代码就可以复现: package main import "log" type T struct { A, B int } func step(t T) T { if t.B != t.A*2 { log.Panic(t) } x := t.A+1 return T{ A: x, B: 2*x, } } func main() { var t = T{ A: 1, B: 2, } for { go func() { t = step(t) }() } } |
25
quzard 2021-11-30 17:38:15 +08:00 via Android
list 没初始化
|
26
ruyiL 2021-11-30 17:41:58 +08:00
这个地方扩容应该是不安全的,但是一般 slice 比较小的时候扩容比较快,所以不容易出问题。
底层的扩容逻辑实际上是开辟一个新数组,然后将 value 拷贝过去,然后将指针指过去,但是当 slice 内存过大之后,这个拷贝的过程是比较漫长的,竞态问题就出现了 |
27
labulaka521 2021-11-30 22:38:03 +08:00
额 推荐下 用 https://go.dev/play 来贴 golang 代码
|
28
sunny1688 OP @labulaka521 感谢
|
29
voocel 2021-12-03 10:38:13 +08:00
slice make 一下应该就不会有问题了
|