为什么这个程序在分配更少的线程时运行得更快?

我有一个相当简单的 Go 程序,旨在计算随机斐波那契数以测试我在我编写的工作池中观察到的一些奇怪行为。当我分配一个线程时,程序在 1.78 秒内完成。当我分配 4 时,它在 9.88 秒内完成。


代码如下:


var workerWG sync.WaitGroup


func worker(fibNum chan int) {

    for {

        var tgt = <-fibNum

        workerWG.Add(1)

        var a, b float64 = 0, 1

        for i := 0; i < tgt; i++ {

            a, b = a+b, a

        }

        workerWG.Done()

    }

}


func main() {

    rand.Seed(time.Now().UnixNano())

    runtime.GOMAXPROCS(1) // LINE IN QUESTION


    var fibNum = make(chan int)


    for i := 0; i < 4; i++ {

        go worker(fibNum)

    }

    for i := 0; i < 500000; i++ {

        fibNum <- rand.Intn(1000)

    }

    workerWG.Wait()

}

如果我替换runtime.GOMAXPROCS(1)为4,则程序的运行时间是原来的四倍。


这里发生了什么?为什么向工作池添加更多可用线程会使整个池变慢?


我个人的理论是,它与工人的处理时间少于线程管理的开销有关,但我不确定。我的预订是由以下测试引起的:


当我用worker以下代码替换函数时:


for {

    <-fibNum

    time.Sleep(500 * time.Millisecond)

}

一个可用线程和四个可用线程占用的时间相同。


蝴蝶不菲
浏览 136回答 3
3回答

婷婷同学_

我修改了你的程序,如下所示:package mainimport (&nbsp; &nbsp; "math/rand"&nbsp; &nbsp; "runtime"&nbsp; &nbsp; "sync"&nbsp; &nbsp; "time")var workerWG sync.WaitGroupfunc worker(fibNum chan int) {&nbsp; &nbsp; for tgt := range fibNum {&nbsp; &nbsp; &nbsp; &nbsp; var a, b float64 = 0, 1&nbsp; &nbsp; &nbsp; &nbsp; for i := 0; i < tgt; i++ {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a, b = a+b, a&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; workerWG.Done()}func main() {&nbsp; &nbsp; rand.Seed(time.Now().UnixNano())&nbsp; &nbsp; runtime.GOMAXPROCS(1) // LINE IN QUESTION&nbsp; &nbsp; var fibNum = make(chan int)&nbsp; &nbsp; for i := 0; i < 4; i++ {&nbsp; &nbsp; &nbsp; &nbsp; go worker(fibNum)&nbsp; &nbsp; &nbsp; &nbsp; workerWG.Add(1)&nbsp; &nbsp; }&nbsp; &nbsp; for i := 0; i < 500000; i++ {&nbsp; &nbsp; &nbsp; &nbsp; fibNum <- rand.Intn(100000)&nbsp; &nbsp; }&nbsp; &nbsp; close(fibNum)&nbsp; &nbsp; workerWG.Wait()}我清理了等待组的使用情况。我改rand.Intn(1000)到rand.Intn(100000)在我的机器上产生:$ time go run threading.go (GOMAXPROCS=1)real&nbsp; &nbsp; 0m20.934suser&nbsp; &nbsp; 0m20.932ssys 0m0.012s$ time go run threading.go (GOMAXPROCS=8)real&nbsp; &nbsp; 0m10.634suser&nbsp; &nbsp; 0m44.184ssys 0m1.928s这意味着在您的原始代码中,执行的工作与同步(通道读/写)相比可以忽略不计。速度减慢来自于必须跨线程而不是一个线程进行同步,并且只在其间执行非常少量的工作。本质上,与计算高达 1000 的斐波那契数相比,同步是昂贵的。这就是人们倾向于不鼓励微基准测试的原因。增加这个数字可以提供更好的视角。但更好的想法是对正在完成的实际工作进行基准测试,即包括 IO、系统调用、处理、处理、写入输出、格式化等。编辑:作为一项实验,我将 GOMAXPROCS 设置为 8 的工人数量增加到 8,结果是:$ time go run threading.go&nbsp;real&nbsp; &nbsp; 0m4.971suser&nbsp; &nbsp; 0m35.692ssys 0m0.044s

慕丝7291255

由于sync.WaitGroup 的原子性,您的代码正在被序列化。双方workerWG.Add(1)并workerWG.Done()会阻塞,直到他们能够更新原子内部计数器。由于工作负载在 0 到 1000 次递归调用之间,单个内核的瓶颈足以将等待组计数器上的数据竞争降至最低。在多核上,处理器花费大量时间旋转来修复等待组调用的冲突。再加上等待组计数器保留在一个核心上,您现在已经添加了核心之间的通信(占用更多周期)。一些简化代码的提示:对于少量的、固定数量的 goroutine,使用完整的通道(chan struct{}以避免分配)更便宜。使用发送通道关闭作为 goroutine 的终止信号,并让它们发出信号,表明它们已退出(等待组或通道)。然后,关闭完成通道以释放它们以供 GC 使用。如果您需要等待组,请尽量减少对其的调用次数。这些调用必须在内部序列化,因此额外的调用会强制添加同步。

慕容708150

您的主要计算例程worker不允许调度程序运行。手动调用调度程序,如&nbsp; &nbsp; for i := 0; i < tgt; i++ {&nbsp; &nbsp; &nbsp; &nbsp; a, b = a+b, a&nbsp; &nbsp; &nbsp; &nbsp; if i%300 == 0 {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; runtime.Gosched()&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }从一个线程切换到两个线程时,挂钟减少 30%。这种人工微基准测试真的很难做到。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go