关于 golang 碰到的一个问题! - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sunny1688
V2EX    问与答

关于 golang 碰到的一个问题!

  •  
  •   sunny1688 2021-11-30 09:30:23 +08:00 3957 次点击
    这是一个创建于 1414 天前的主题,其中的信息可能已经有所发展或是发生改变。

    直接上代码,请看图: https://pic.baixiongz.com/uploads/2021/11/30/bc91319946394.jpeg

    搞不明白为什么 append slice 会 panic ,出现空指针,而且不是必现,运行一段时间才会出现,一般在几个小时内,求大佬解释一下是为什么

    第 1 条附言    2021-12-01 17:07:35 +08:00
    29 条回复    2021-12-03 10:38:13 +08:00
    longfxxx
        1
    longfxxx  
       2021-11-30 09:41:47 +08:00 via iPhone
    slice 不需要 make 一下吗?
    whitehack
        2
    whitehack  
       2021-11-30 09:42:54 +08:00   1
    共享了那个 list 变量 没加锁
    你先加个锁 还有问题再来问
    sujin190
        3
    sujin190  
       2021-11-30 09:43:44 +08:00
    你这图和你右边的输出似乎没啥关系吧
    mangoDB
        4
    mangoDB  
       2021-11-30 09:44:10 +08:00
    slice 不是 thread safe 的。
    sujin190
        5
    sujin190  
       2021-11-30 09:46:28 +08:00
    @longfxxx #1 会自动初始化的
    @whitehack #2 不加锁并不会 panic ,只是添加的数量不对,估计右边 panic 显然不是左边这个代码能产生的
    sadfQED2
        6
    sadfQED2  
       2021-11-30 09:49:55 +08:00 via Android
    @sujin190 你图片看不到。golang 里面不加锁会 panic
    不过空指针应该不是加群的问题,检查下是不是并发情况导致没有初始化
    driveby
        7
    driveby  
       2021-11-30 09:53:35 +08:00
    你这不加锁不是已经 panic 了吗。应该就是 slice 没加锁的原因,照 #2 的方式多跑几遍对照一下就知道结论了。
    iyear
        8
    iyear  
       2021-11-30 09:54:09 +08:00 via Android
    @mangoDB 那也不会报空指针的错吧
    PungentSauce
        9
    PungentSauce  
       2021-11-30 09:54:54 +08:00
    你这是把内存跑满了吧
    sujin190
        10
    sujin190  
       2021-11-30 09:58:13 +08:00
    @sadfQED2 #6 但是实际测试了并不会,不要猜测啊
    sunny1688
        11
    sunny1688  
    OP
       2021-11-30 10:00:43 +08:00
    @PungentSauce 内存没跑满,跑个一会就会出现,不是立马复现
    @mangoDB 对,不是线程安全,最终也是 append 的数量不对,但也不应该是空指针
    @longfxxx struct 会自动初始化,有零值,可以直接 append
    sunny1688
        12
    sunny1688  
    OP
       2021-11-30 10:01:46 +08:00
    ```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)
    }
    }
    ```

    这是代码,大家可以跑一段时间,然后看看会不会出现空指针
    imherer
        13
    imherer  
       2021-11-30 10:03:47 +08:00   4
    append 后 slice 如果扩容会导致 demo.list 的地址发生变化
    sujin190
        14
    sujin190  
       2021-11-30 10:07:54 +08:00
    @imherer #13 然后原地址可能已经被回收,但因协程调度原因此时有协程才刚开始使用原地址进行操作这样么?嗯,极高并发下看起来还真有可能
    imherer
        15
    imherer  
       2021-11-30 10:09:16 +08:00
    @sujin190 是的
    mangoDB
        16
    mangoDB  
       2021-11-30 10:10:35 +08:00
    @iyear 我认为 slice 的地址会不断发生变化(因为扩容),在「竞争」的背景下,某个协程拿到的地址不一定是有效的。
    sujin190
        17
    sujin190  
       2021-11-30 10:18:56 +08:00
    @imherer #15 但是把如果是 c 和 c++的话,原地址被回收只是代表其会被重用于其它内存分配,地址指向的物理内存是不会消失的,所以也就不会出现空指针错误,除非这是一个双重指针,地址回收的时候更新了第二层指针的指向为空

    说起来实际使用来看,go 还真是这么设计的,双重指针,只是这样设计似乎效率低了一点,但是好处确实是保证不会突破内存屏障了,上层使用来看确实有些地方还是很让人莫名其妙的
    iyear
        18
    iyear  
       2021-11-30 10:23:05 +08:00 via Android
    @mangoDB 很有道理感谢
    sunny1688
        19
    sunny1688  
    OP
       2021-11-30 10:30:02 +08:00
    @imherer @mangoDB 感谢大神,感谢大神,终于解惑了!
    sxfscool
        20
    sxfscool  
       2021-11-30 10:43:01 +08:00
    先加个锁
    jimmzhou
        21
    jimmzhou  
       2021-11-30 10:56:11 +08:00
    go run -race 跑一下 会发现 WARNING: DATA RACE
    loushizan
        22
    loushizan  
       2021-11-30 14:02:44 +08:00
    @mangoDB
    不过准确的说,是 slice 指向的数组指针发生了变化
    type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
    }
    Data 发生了变化,slice 本身不会
    Marmot
        23
    Marmot  
       2021-11-30 14:13:55 +08:00
    @imherer 这个老哥回答的才是对的,也是上面说的为什么需要加锁的原因,slice 的底层是一个数组,当触发扩容之后,会把内容 copy 到新的内存地址上面去,然后 gc 回收旧的那个,但是有些 gorountie 还在往上面写
    icexin
        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)
    }()
    }
    }
    quzard
        25
    quzard  
       2021-11-30 17:38:15 +08:00 via Android
    list 没初始化
    ruyiL
        26
    ruyiL  
       2021-11-30 17:41:58 +08:00
    这个地方扩容应该是不安全的,但是一般 slice 比较小的时候扩容比较快,所以不容易出问题。
    底层的扩容逻辑实际上是开辟一个新数组,然后将 value 拷贝过去,然后将指针指过去,但是当 slice 内存过大之后,这个拷贝的过程是比较漫长的,竞态问题就出现了
    labulaka521
        27
    laulaka521  
       2021-11-30 22:38:03 +08:00
    额 推荐下 用 https://go.dev/play 来贴 golang 代码
    sunny1688
        28
    sunny1688  
    OP
       2021-12-01 17:06:15 +08:00
    @labulaka521 感谢
    voocel
        29
    voocel  
       2021-12-03 10:38:13 +08:00
    slice make 一下应该就不会有问题了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5960 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 02:51 PVG 10:51 LAX 19:51 JFK 22:51
    Do have faith in what you're doing.
    ubao 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