golang runtime实现多核并行任务

golang runtime实现多核并行任务

 

    首先我们需要明确一下并行跟并发的区别,并发一般是被内核通过时间片或者中断来控制的,遇到io阻塞或者时间片用完的时会转移线程的使用权。一个核的情况下不可能有并行的情况,因为同一时间只有一个任务在调度。  

 

该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。

http://xiaorui.cc/2016/03/05/golang-runtime%E5%AE%9E%E7%8E%B0%E5%A4%9A%E6%A0%B8%E5%B9%B6%E8%A1%8C%E4%BB%BB%E5%8A%A1/

 

    Golang默认所有的任务都在一个cpu核里,如果想使用多核来跑goroutine的任务,需要配置runtime.GOMAXPROCS。 GOMAXPROCS的数目根据自己任务量分配就可以了,有个前提是不要大于你的cpu核数。 并行比较适合那种cpu密集型计算,如果是IO密集型使用多核的化会增加cpu切换的成本。

 

 

 

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

26

27

28

29

30

31

32

33

package main

//xiaorui.cc

 

import (

"fmt"

"runtime"

)

 

func test(c chan bool, n int) {

 

x := 0

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

x += i

}

println(n, x)

if n == 10 {

c <- true

}

}

 

func main() {

runtime.GOMAXPROCS(3)

c := make(chan bool)

 

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

go test(c, i)

}

 

<-c

 

fmt.Println("main end...")

 

}

 

 

对于经常使用python multiprocessing多进程模块来跑多核多任务的我来说,不再有GIL全局锁是个很美妙的事情。   我们通过top可以看到上面的实例代码跑到了cpu 270% . 跟我们上面配置的runtime.GOMAXPROCS(3)相对应。 

对于goroutine任务的终止也是有技巧的,他不能像多进程那样,直接给kill掉。  他只能是通过类似flag信号控制,每个goroutine执行的函数逻辑里都要判断flag标示位是否为stop状态。

 

需要特意说明一点是,你在测试并发的时候,往往会把一个函数写成死循环并做计算,你虽然这段函数用go关键词并发了,但是你会发现他无法执行后面的逻辑。你需要做的是配置多核,或者是time.Sleep()。  这是为什么 ? Golang是自己管理调整goroutine,如果你的一个func始终不释放资源,那么其他的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

26

27

28

29

30

package main

 

import (

"fmt"

"runtime"

_ "time"

)

 

var (

flag = false

str  string

)

 

func xiaorui() {

flag = true

str = "setup flag to true"

}

 

func main() {

runtime.GOMAXPROCS(1)

go xiaorui()

//time.Sleep(1 * time.Second)

       // 理论来说,当我在xiaorui()把flag 改为true后,后面的逻辑会退出.

for {

if flag {

break

}

}

fmt.Println(str)

}

 

 

 

我们不断的调整的runtime.GOMAXPROCS(num) ,会发现执行的速度越来越快,但不要超过你的cpu数,因为那是徒劳的。 同样的代码我用python multiprocessing pool也实现了一份,我在MAC和线上服务器做过测试,性能要远高于python。

<think>我们正在讨论Golang的GMP模型中的工作窃取机制。根据引用[3],当某个P的本地队列为空且全局队列也没有G时,它会尝试从其他P的本地队列中偷取一半的G。具体来说,偷取者会从其他P的本地队列尾部取一半的G,放到自己的本地队列中执行。 另外,引用[4]展示了P的结构体,其中包含一个本地运行队列(runq)以及队列的头尾指针。引用[5]则说明了G(goroutine)的各种状态。 因此,我们可以详细解释工作窃取机制如下: 1. 当某个P(假设为P2)完成了自己本地队列中的所有G,并且全局队列中也没有G时,它会开始工作窃取。 2. 它会随机选择一个其他的P(比如P1),然后从P1的本地队列尾部偷取一半的G。例如,如果P1的本地队列中有N个G,那么偷取的数量为(N/2)向上取整(实际上代码实现中会偷取一半,但具体细节可能更复杂)。 3. 偷取来的G会被放入P2的本地队列中,然后P2就可以开始执行这些G。 这样做的好处是平衡各个P之间的负载,避免某些P空闲而其他P过载。 下面我们结合引用内容详细说明: 引用[3]提到:“全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。” 引用[4]中P的结构体有一个固定大小的循环队列(runq)来存储G,大小为256。另外还有一个runnext字段,用于存放下一个要执行的G(高优先级)。 工作窃取机制在源码中的实现位于`runtime/proc.go`文件中的`stealWork`函数。但具体细节我们不必深究,只需理解其原理。 总结工作窃取步骤: - 步骤1:P检查自己的本地队列,为空则检查全局队列(需要加锁),全局队列也为空则开始工作窃取。 - 步骤2:随机选择一个P,尝试从其本地队列中偷取一半的G(从队列尾部偷取,这样不会影响该P正在执行的G,因为通常P从队列头部取G执行)。 - 步骤3:如果偷取成功,则将偷来的G放入自己的本地队列,然后开始执行。 - 步骤4:如果偷取失败(其他P的队列也空),则继续随机选择另一个P,直到尝试过所有P(最多尝试四次)。 注意:工作窃取可以避免全局队列的锁竞争,因为每个P的本地队列操作不需要全局锁(除了偷取时需要锁住被偷的P的队列)。 此外,引用[2]提到:“p 是 gmp 的中枢,借由 p 承上启下,实现 g 和 m 之间的动态有机结合”。工作窃取正是由P来协调的,M(线程)通过绑定的P来获取G。 下面我们尝试用伪代码描述工作窃取的大致逻辑(非真实代码,仅示意): ```go // 当P本地队列为空时,尝试获取G func findRunnable() (gp *g) { // 检查本地队列、全局队列等 // ... if 全局队列为空 { // 尝试工作窃取 for i := 0; i < 4; i++ { // 最多尝试4次 // 随机选择一个P p2 := randomP() // 从p2的本地队列尾部偷取一半的G half := (len(p2.runq) + 1) / 2 // 向上取整 if half > 0 { // 偷取half个G gs := p2.stealFromTail(half) // 将偷来的G放入自己的本地队列 thisP.enqueue(gs) // 取出一个G执行 return thisP.dequeue() } } } // 如果还是没偷到,则进入休眠等 // ... } ``` 实际源码中,偷取操作会考虑队列的平衡,避免一次性偷取过多导致被偷P的负载突然下降。 最后,工作窃取机制是许多并发调度系统中常见的负载均衡策略,在Go的GMP模型中,它帮助多个P之间平衡G的执行任务,从而更充分地利用多核CPU资源。</think>### Golang GMP模型中的工作窃取机制详解 在Go语言的GMP调度模型中,**工作窃取(Work Stealing)** 是实现负载均衡的核心机制。当某个处理器(P)的本地任务队列为空时,它会从其他繁忙的P中"窃取"一部分Goroutine(G)来执行,从而最大化CPU利用率。以下是详细解析: #### 1. **触发条件** - 当P的本地队列为空(`runq`为空且`runnext`为nil) - 全局队列也没有待执行的G - 此时P会进入工作窃取状态[^3] #### 2. **窃取流程** ```mermaid graph LR A[P空闲] --> B{检查全局队列} B -- 为空 --> C[随机选择目标P] C --> D[从目标P本地队列尾部窃取] D --> E{窃取数量?} E -- 成功 --> F[取目标队列一半G] E -- 失败 --> C F --> G[放入窃取者本地队列] ``` 具体步骤: 1. **随机选择目标P**:从其他活跃P中随机选择一个 2. **尾部窃取**:从目标P本地队列**尾部**取走一半G(例如目标队列有8个G,则窃取4个)[^3] 3. **负载均衡**:优先窃取队列较长的P,避免频繁打扰短队列 4. **失败重试**:最多尝试`4`次随机选择(源码中`stealWork`函数的`max steal tries`) #### 3. **关键设计特点** - **尾部窃取优势**: - 减少锁竞争:目标P通常从队列**头部**取G执行,尾部操作冲突小 - 保持缓存局部性:被窃取的G通常是新创建的,执行状态更完整[^4] - **原子操作**:通过`runqhead/runqtail`的CAS操作避免全局锁[^4] - **窃取量计算**:`n = min(len(targetQ)/2, len(targetQ)-1)` 确保留至少1个G给目标P #### 4. **源码实现要点** - **窃取入口**:`runtime.schedule() → runtime.findrunnable() → runtime.stealWork()` - **队列操作**(参考P结构体[^4]): ```go type p struct { runqhead uint32 // 环形队列头指针 runqtail uint32 // 环形队列尾指针 runq [256]guintptr // 固定大小本地队列 runnext guintptr // 高优先级插队G } ``` - **窃取算法**: ```go // 伪代码简化 func stealWork(nowP) { for i := 0; i < 4; i++ { // 最多尝试4次 targetP := randomP() n := targetP.runqCount() / 2 // 计算窃取量 if n > 0 { gs := targetP.popTail(n) // 原子操作取尾部G nowP.push(gs) // 加入窃取者队列 return } } } ``` #### 5. **性能影响** - **优点**: - 减少线程阻塞:避免M(线程)因无任务而休眠 - 提升并行度:快速平衡各P间负载,尤其适合任务量不均衡的场景 - **限制**: - 全局队列优先级低:只有当所有P本地队列为空时才检查全局队列 - 窃取开销:随机选择目标P可能命中空队列(通过重试机制缓解) #### 6. **设计哲学** 工作窃取机制完美体现了GMP模型的调度原则:**"当本地资源不足时,协作式地获取外部资源,而非被动等待"**。通过P作为中介(如引用[2]所述:"P是GMP的中枢"),实现了G与M的动态解耦,使G能跨M执行[^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值