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

时间:2015-09-16 06:03:44

标签: multithreading go concurrency worker

我有一个相当简单的Go程序,旨在计算随机Fibonacci数,以测试我在我编写的工作池中观察到的一些奇怪行为。 当我分配一个线程时,程序在1.78s完成。当我分配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)
}

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

3 个答案:

答案 0 :(得分:3)

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

 IF OBJECT_ID('tempdb..#tmpp1') IS NOT NULL
       DROP TABLE #tmpp1;

                SELECT  
                     [PlanningHierarchyId] 
                    ,[ProductReferenceId]
                    ,[PlanningYear] 
                    ,[PlanningSeason]
                    ,[UpdatedBy] 
                    INTO #tmpp1
                FROM @paramTable
  • 我清理了等待组的使用情况。
  • 我将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)

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

实质上,与计算高达1000的斐波那契数相比,同步是昂贵的。这就是为什么人们倾向于阻止微基准测试。提高这个数字可以提供更好的视角。但更好的想法是对正在进行的实际工作进行基准测试,包括IO,系统调用,处理,运算,写输出,格式化等。

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

$ 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

答案 1 :(得分:1)

@thwd编写的代码是正确的,也是惯用的Go。

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

  • 由于工作负载在0到1000个递归调用之间,因此单个内核的瓶颈足以使waitgroup计数器上的数据竞争保持最小。
  • 在多核上,处理器花费大量时间来修复waitgroup调用的冲突。再加上一个事实,即waitgroup计数器保存在一个核心上,你现在已经在核心之间增加了通信(占用了更多的周期)。

一些简化代码的提示:

  • 对于一小组固定数量的goroutine,一个完整的通道(chan struct{}以避免分配)使用起来更便宜。
  • 使用发送通道关闭作为goroutines的终止信号,让他们发出信号表示已退出(waitgroup或channel)。然后,关闭完整的通道以释放它们用于GC。
  • 如果您需要等待组,请积极减少对其的呼叫次数。这些调用必须在内部序列化,因此额外的调用会强制增加同步。

答案 2 :(得分:-1)

worker中的主计算例程不允许调度程序运行。 像

一样手动调用调度程序
    for i := 0; i < tgt; i++ {
        a, b = a+b, a
        if i%300 == 0 {
            runtime.Gosched()
        }
    }

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

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