IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Go单线程运行也会有并发问题

    smallnest发表于 2023-12-13 13:09:16
    love 0

    一个Go大佬群中严肃的讨论了一个问题:Go程序单线程多goroutine访问一个map会遇到并发读写panic么?

    答案是肯定的,因为出现了这个问题所以大家才在群中讨论。

    为什么呢?因为单线程意味着并行单元只有一个(多线程也可能并行单元只有一个),但是多goroutine意味着并发单元有多个,如果并发单元同时执行,即使是单线程,可能就会产生数据竞争的问题,除非这些goroutine是顺序执行的。

    举一个例子哈:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func TestCounter() {
    runtime.GOMAXPROCS(1)
    var counter int
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
    i := i
    go func() {
    fmt.Printf("start task#%d, counter: %d\n", i, counter)
    for j := 0; j < 10_0000; j++ {
    counter++
    }
    fmt.Printf("end task#%d, counter: %d\n", i, counter)
    wg.Done()
    }()
    }
    wg.Wait()
    fmt.Println(counter)
    }

    这段测试代码是启动10个goroutine对计数器加一,每个goroutine负责加10万次。在我的MBP m1笔记本上,每次的结果都是100万,符合期望。如果你运行这段代码,会发现goroutine其实是一个一个串行执行的(9->0->1->2->3->4->5->6->7->8,当然可能在你的机器上不是这样的),如果是串行执行,不会有并发问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    start task#9, counter: 0
    end task#9, counter: 100000
    start task#0, counter: 100000
    end task#0, counter: 200000
    start task#1, counter: 200000
    end task#1, counter: 300000
    start task#2, counter: 300000
    end task#2, counter: 400000
    start task#3, counter: 400000
    end task#3, counter: 500000
    start task#4, counter: 500000
    end task#4, counter: 600000
    start task#5, counter: 600000
    end task#5, counter: 700000
    start task#6, counter: 700000
    end task#6, counter: 800000
    start task#7, counter: 800000
    end task#7, counter: 900000
    start task#8, counter: 900000
    end task#8, counter: 1000000
    1000000

    为了制造点紧张气氛,我将代码改写成下面这样子,将counter++三条指令明显写成三条语句,并在中间插入runtime.Gosched(),故意给其它goroutine的执行制造机会:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    func TestCounter2() {
    runtime.GOMAXPROCS(1)
    var counter int
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
    i := i
    go func() {
    fmt.Printf("start task#%d, counter: %d\n", i, counter)
    for j := 0; j < 10_0000; j++ {
    temp := counter
    runtime.Gosched()
    temp = temp + 1
    counter = temp
    }
    fmt.Printf("end task#%d, counter: %d\n", i, counter)
    wg.Done()
    }()
    }
    wg.Wait()
    fmt.Println(counter)
    }

    运行这段代码,你就会明显看到数据不一致的效果,即使是单个线程运行goroutine,也出现了数据竞争的问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    start task#9, counter: 0
    start task#0, counter: 0
    start task#1, counter: 0
    start task#2, counter: 0
    start task#3, counter: 0
    start task#4, counter: 0
    start task#5, counter: 0
    start task#6, counter: 0
    start task#7, counter: 0
    start task#8, counter: 0
    end task#9, counter: 100000
    end task#1, counter: 100000
    end task#3, counter: 100000
    end task#2, counter: 100000
    end task#5, counter: 100000
    end task#0, counter: 100000
    end task#4, counter: 100000
    end task#6, counter: 100000
    end task#7, counter: 100000
    end task#8, counter: 100000
    100000

    这个结果非常离谱,期望100万,最后只有10万。

    因为单个线程运行多个goroutine会有数据竞争的问题,所以访问同一个map对象也有可能出现并发bug,比如下面的代码,10个goroutine并发的写同一个map:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func TestMap() {
    var m = make(map[int]int)
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
    i := i
    go func() {
    fmt.Printf("start map task#%d, m: %v\n", i, len(m))
    for j := 0; j < 10_0000; j++ {
    m[j] = i*10_0000 + j
    }
    fmt.Printf("end map task#%d, m: %v\n", i, len(m))
    wg.Done()
    }()
    }
    wg.Wait()
    }

    大概率会出现panic:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    start map task#9, m: 0
    start map task#0, m: 49152
    fatal error: concurrent map writes
    goroutine 41 [running]:
    main.TestMap.func1()
    /Users/chaoyuepan/study/single_thread/main.go:72 +0xcc
    created by main.TestMap in goroutine 1
    /Users/chaoyuepan/study/single_thread/main.go:69 +0x4c
    goroutine 1 [semacquire]:
    sync.runtime_Semacquire(0x140000021a0?)
    /usr/local/go/src/runtime/sema.go:62 +0x2c
    sync.(*WaitGroup).Wait(0x1400000e1d0)
    /usr/local/go/src/sync/waitgroup.go:116 +0x74
    main.TestMap()
    /Users/chaoyuepan/study/single_thread/main.go:79 +0xb8
    main.main()
    /Users/chaoyuepan/study/single_thread/main.go:15 +0x2c


沪ICP备19023445号-2号
友情链接