V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
sunny1688
V2EX  ›  问与答

关于 golang 碰到的一个问题!

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

    直接上代码,请看图: 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
    labulaka521  
       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   ·   实用小工具   ·   2516 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 03:47 · PVG 11:47 · LAX 19:47 · JFK 22:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.