goroutine的多核并行化,让出时间片

Golang
415
0
0
2022-07-17

1.多核并行

Go 1.5开始, Go的GOMAXPROCS默认值已经设置为 CPU的核数, 这允许我们的Go程序充分使用机器的每一个CPU,最大程度的提高我们程序的并发性能。

runtime.GOMAXPROCS(16)

到底应该设置多少个CPU核心呢,其实runtime包中还提供了另外一个函数NumCPU()来获

取核心数。可以看到,Go语言其实已经感知到所有的环􏲁信息,下一版本中完全可以利用这些 信息将goroutine调度到所有CPU核心上,从而最大化地利用服务器的多核计算能力。弃 GOMAXPROCS只是个时间问题。

    fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU()) //runtime.NumCPU(): 12

看下面一段简单的代码

package main

import (
    "fmt" 
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

尝试运行发现A,B的输出并没有规律,带有随机性

运行结果:

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
A
B
A
B
A
B
A
B
B
B
A
A
B
A
A
B
B
[Done] exited with code=0 in 0.364 seconds

证明,每次循环开启的两个go程是并行的,因为在目前我的这个版本,默认 Go的GOMAXPROCS默认值已经设置为 CPU的核数,如果我设置Go的GOMAXPROCS为1,代表这些goroutine都运行在一个cpu核心上,在一个goroutine得到时间片执行的时候,其他goroutine 都会处于等待状态

package main

import (
    "fmt" 
    "runtime" 
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

设置runtime.GOMAXPROCS(1)后,再次运行,不管运行几次,都是两个交替输出,很规律

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
[Done] exited with code=0 in 0.353 seconds

因为每次循环创建的go程,都在同一个cpu核心上,对应GPM模型的P队列只有一个,需要排队执行,所以就出现了上面的输出结果

2.让出时间片

我们可以在每个goroutine中控制何时主动出让时间片给其他goroutine,这可以使用runtime 包中的Gosched()函数实现。
实际上,如果要比较精细地控制goroutine的行为,就必须比较深入地了解Go语言开发包中 runtime包所提供的具体功能。

我们依旧是对上面的代码进行改造:(同样是 Go的GOMAXPROCS为1,goroutine p队列为1,多个go程无法并行的情况下)

package main

import (
    "fmt" 
    "runtime" 
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    runtime.Gosched() //打印B之前,让出当前goroutine所占的时间片
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

看到上面的代码,我们在打印B之前,让出当前goroutine所占的时间片,这个输出结果会是什么呢?

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/tempCodeRunnerFile.go"
A
A
A
A
A
A
A
A
A
A
B
B
B
B
B
B
B
B
B
B
[Done] exited with code=0 in 0.574 seconds

可以看到,先把A全部打印,然后才去打印的B,因为每次循环开启的两个goroutine,是交替执行,当执行myPrintB的go协程抢到时间片的时候,在内部,执行 fmt.Println("B")之前,将当前goroutine,抢到的时间片让出,保存当前的状态,等再次抢到了时间片,就继续执行,这里可能会有点小疑问(那为什么当前循环的B没有继续执行呢?而且全部先执行的输出A呢?)针对这个疑问,先留下一个猜想(当前时间片让出后,被下一个循环的goroutine抢去了,如果当前循环的时间足够的话(不会那么快进行到下次循环,就不会创建新的goroutine),可能就可以在当前循环中执行了),下面我们就去证实我们的猜想,我们在每次循环中,让程序sleep 1s

package main

import (
    "fmt" 
    "runtime" 
    "sync" 
    "time"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    runtime.Gosched()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
        time.Sleep(time.Second)
    }
    wg.Wait()
}

输出结果:隔一秒,输出一次A,B,(B让出时间片后,还能再次抢到时间片继续执行自己下面代码,因为当前有足够的时间和空闲的时间片给他用,不会那么快被下次循环创建的goroutine抢去!)

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
[Done] exited with code=0 in 10.569 seconds