Golang当将GOMAXPROCS设置为1时会发生什么

请尝试在评论区里写下答案(如不能清楚表述,那么你可能没真正理解)。欢迎参与,为下一次求职做准备。

最近面试的时候出了一道题,没有打上来,后面研究了一下,在这里分享出来

    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B", i)
            wg.Done()
        }(i)
    }

    wg.Wait()

以上这段代码会输出什么?
我的golang的版本是1.17.4,不同的版本可能输出的不同,这里我只提我自己的版本,输出结果为:

B 9
A 10
A 10
A 10
A 10
A 10
A 10
A 10
A 10
A 10
A 10
B 0
B 1
B 2
B 3
B 4
B 5
B 6
B 7
B 8

这里我们看到A的话,全是10,B的话,9是第一个执行,后面基本上全是顺序执行了,那造成这个情况的原因是什么呢?话不多说,直接看源码,当我们在用一个go的关键词时,编译器会给go解析为newproc函数,在runtime/proc.go中
大致意思就是创建一个新的g来运行fn,size为参数的大小,然后把他放入到等待运行的g队列中,newproc1就是来创建一个新的g的

func newproc(siz int32, fn *funcval) {
    argp := add(unsafe.Pointer(&fn), sys.PtrSize)
    gp := getg()
    pc := getcallerpc()
    systemstack(func() {
        newg := newproc1(fn, argp, siz, gp, pc)

        _p_ := getg().m.p.ptr()
        runqput(_p_, newg, true)

        if mainStarted {
            wakep()
        }
    })
}

runqput是将g放入到本地可运行队列中,如果第三个参数,next为true的话,runqput将会把g放入到p.runnext上,如果为false的话,将g添加到可运行队列的尾部,在runnext中的g永远会被最先调度执行

func runqput(_p_ *p, gp *g, next bool) {
    if randomizeScheduler && next && fastrand()%2 == 0 {
        next = false
    }

    if next {
    retryNext:
        oldnext := _p_.runnext
        // 把g放入到_p_.runnext上
        if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {
            goto retryNext
        }
        if oldnext == 0 {
            return
        }
        gp = oldnext.ptr()
    }
//将原_p_.runnext上的g放入到队列_p_.runq中
retry:
    h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumers
    t := _p_.runqtail
    if t-h < uint32(len(_p_.runq)) {
        _p_.runq[t%uint32(len(_p_.runq))].set(gp)
        atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
        return
    }
    if runqputslow(_p_, gp, h, t) {
        return
    }
    goto retry
}

看到这里就是创建一个协程的过程,但是需要解答这个题目,还需要掌握一个知识,在 for i := 0; i < 10; i++ 遍历中, i 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 i,i 的内存地址始终不变。

当看到这里,不知道大家理解了没有,当我们给线程数量设置为1时,循环的创建协程时,协程会优先放入到p.runnext上,后续新的协程创建时,会将新的协程再次放入到p.runnext上,然后再将之前的放入p.runq中,我们上面的程序执行下来,最后B9会放入到p.runnext上,所以会优先打印出来B9,然后在打印A,A读取i时,i的值已经递增到了10,所以A打印出来全部是10,而B的话,是传递的参数,会保存起来,所以B的话,是从0-8顺序执行。

大致执行的逻辑是这样,有需要的小伙伴可以尝试阅读源码,如果文中有描述错误的地方,望指教,谢谢观看

讨论数量: 1

GOMAXPROCS 设置的是 GMP 模型中 P 的数量,如果设置为 1,那么在程序运行的任意时刻只能有一个 G 在运行。可以存在多个 M 和 G,但是同一时刻只能有一个 G 运行。

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!