为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

Go
蝴蝶不菲 2021-11-08 15:50:59
我有一个相当简单的 Go 程序,旨在计算随机斐波那契数以测试我在我编写的工作池中观察到的一些奇怪行为。当我分配一个线程时,程序在 1.78 秒内完成。当我分配 4 时,它在 9.88 秒内完成。代码如下:var workerWG sync.WaitGroupfunc 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)}一个可用线程和四个可用线程占用的时间相同。
查看完整描述

3 回答

?
婷婷同学_

TA贡献1844条经验 获得超8个赞

我修改了你的程序,如下所示:


package main


import (

    "math/rand"

    "runtime"

    "sync"

    "time"

)


var workerWG sync.WaitGroup


func worker(fibNum chan int) {

    for tgt := range fibNum {

        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)

        workerWG.Add(1)

    }

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

        fibNum <- rand.Intn(100000)

    }

    close(fibNum)

    workerWG.Wait()

}

我清理了等待组的使用情况。

我改rand.Intn(1000)到rand.Intn(100000)

在我的机器上产生:


$ time go run threading.go (GOMAXPROCS=1)


real    0m20.934s

user    0m20.932s

sys 0m0.012s


$ time go run threading.go (GOMAXPROCS=8)


real    0m10.634s

user    0m44.184s

sys 0m1.928s

这意味着在您的原始代码中,执行的工作与同步(通道读/写)相比可以忽略不计。速度减慢来自于必须跨线程而不是一个线程进行同步,并且只在其间执行非常少量的工作。


本质上,与计算高达 1000 的斐波那契数相比,同步是昂贵的。这就是人们倾向于不鼓励微基准测试的原因。增加这个数字可以提供更好的视角。但更好的想法是对正在完成的实际工作进行基准测试,即包括 IO、系统调用、处理、处理、写入输出、格式化等。


编辑:作为一项实验,我将 GOMAXPROCS 设置为 8 的工人数量增加到 8,结果是:


$ time go run threading.go 


real    0m4.971s

user    0m35.692s

sys 0m0.044s


查看完整回答
反对 回复 2021-11-08
?
慕丝7291255

TA贡献1859条经验 获得超6个赞

由于sync.WaitGroup 的原子性,您的代码正在被序列化。双方workerWG.Add(1)workerWG.Done()会阻塞,直到他们能够更新原子内部计数器。

  • 由于工作负载在 0 到 1000 次递归调用之间,单个内核的瓶颈足以将等待组计数器上的数据竞争降至最低。

  • 在多核上,处理器花费大量时间旋转来修复等待组调用的冲突。再加上等待组计数器保留在一个核心上,您现在已经添加了核心之间的通信(占用更多周期)。

一些简化代码的提示:

  • 对于少量的、固定数量的 goroutine,使用完整的通道(chan struct{}以避免分配)更便宜。

  • 使用发送通道关闭作为 goroutine 的终止信号,并让它们发出信号,表明它们已退出(等待组或通道)。然后,关闭完成通道以释放它们以供 GC 使用。

  • 如果您需要等待组,请尽量减少对其的调用次数。这些调用必须在内部序列化,因此额外的调用会强制添加同步。


查看完整回答
反对 回复 2021-11-08
?
慕容708150

TA贡献1831条经验 获得超4个赞

您的主要计算例程worker不允许调度程序运行。手动调用调度程序,如


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

        a, b = a+b, a

        if i%300 == 0 {

            runtime.Gosched()

        }

    }

从一个线程切换到两个线程时,挂钟减少 30%。


这种人工微基准测试真的很难做到。


查看完整回答
反对 回复 2021-11-08
  • 3 回答
  • 0 关注
  • 135 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信