Go语言具有支持高并发的特性,可以很方便地实现多线程运算,充分利用多核心 cpu 的性能。
众所周知服务器的处理器大都是单核频率较低而核心数较多,对于支持高并发的程序语言,可以充分利用服务器的多核优势,从而降低单核压力,减少性能浪费。
Go语言实现多核多线程并发运行是非常方便的,下面举个例子:
package main
import (
"fmt"
)
func main() {
for i := 0; i < 5; i++ {
go AsyncFunc(i)
}
}
func AsyncFunc(index int) {
sum := 0
for i := 0; i < 10000; i++ {
sum += 1
}
fmt.Printf("线程%d, sum为:%d\n", index, sum)
}
运行结果如下:
在执行一些昂贵的计算任务时,我们希望能够尽量利用现代服务器普遍具备的多核特性来尽量将任务并行化,从而达到降低总计算时间的目的。此时我们需要了解 CPU 核心的数量,并针对性地分解计算任务到多个 goroutine 中去并行运行。
下面我们来模拟一个完全可以并行的计算任务:计算 N 个整型数的总和。我们可以将所有整型数分成 M 份,M 即 CPU 的个数。让每个 CPU 开始计算分给它的那份计算任务,最后将每个 CPU 的计算结果再做一次累加,这样就可以得到所有 N 个整型数的总和:
type Vector []float64
// 分配给每个CPU的计算任务
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // 发信号告诉任务管理者我已经计算完成了
}
const NCPU = 16 // 假设总共有16核
func (v Vector) DoAll(u Vector) {
c := make(chan int, NCPU) // 用于接收每个CPU的任务完成信号
for i := 0; i < NCPU; i++ {
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
// 等待所有CPU的任务完成
for i := 0; i < NCPU; i++ {
<-c // 获取到一个数据,表示一个CPU计算完成了
}
// 到这里表示所有计算已经结束
}
这两个函数看起来设计非常合理,其中 DoAll() 会根据 CPU 核心的数目对任务进行分割,然后开辟多个 goroutine 来并行执行这些计算任务。
是否可以将总的计算时间降到接近原来的 1/N 呢?答案是不一定。如果掐秒表,会发现总的执行时间没有明显缩短。再去观察 CPU 运行状态,你会发现尽管我们有 16 个 CPU 核心,但在计算过程中其实只有一个 CPU 核心处于繁忙状态,这是会让很多Go语言初学者迷惑的问题。
官方给出的答案是,这是当前版本的 Go 编译器还不能很智能地去发现和利用多核的优势。虽然我们确实创建了多个 goroutine,并且从运行状态看这些 goroutine 也都在并行运行,但实际上所有这些 goroutine 都运行在同一个 CPU 核心上,在一个 goroutine 得到时间片执行的时候,其他 goroutine 都会处于等待状态。从这一点可以看出,虽然 goroutine 简化了我们写并行代码的过程,但实际上整体运行效率并不真正高于单线程程序。
虽然Go语言还不能很好的利用多核心的优势,我们可以先通过设置环境变量 GOMAXPROCS 的值来控制使用多少个 CPU 核心。具体操作方法是通过直接设置环境变量 GOMAXPROCS 的值,或者在代码中启动 goroutine 之前先调用以下这个语句以设置使用 16 个 CPU 核心:
到底应该设置多少个 CPU 核心呢,其实 runtime 包中还提供了另外一个 NumCPU() 函数来获取核心数,示例代码如下:
package main
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU() //获得当前设备的cpu核心数
fmt.Println("cpu核心数:", cpuNum)
runtime.GOMAXPROCS(cpuNum) //设置需要用到的cpu数量
}
运行结果如下: