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