多核并行化

在 Go 语 言 升 级 到 默 认 支 持 多 CPU 的 某 个 版 本 之 前 , 我 们 可 以 先 通 过 设 置 环 境 变 量GOMAXPROCS 的值来控制使用多少个CPU核心。具体操作方法是通过直接设置环境变量GOMAXPROCS的值,或者在代码中启动goroutine之前先调用以下这个语句以设置使用16个CPU核心:

  1. runtime.GOMAXPROCS(16)

到底应该设置多少个CPU核心呢,其实runtime包中还提供了另外一个函数NumCPU()来获取核心数。可以看到, Go语言其实已经感知到所有的环境信息,下一版本中完全可以利用这些信息将goroutine调度到所有CPU核心上,从而最大化地利用服务器的多核计算能力。抛弃GOMAXPROCS只是个时间问题。

出让时间片

我们可以在每个goroutine中控制何时主动出让时间片给其他goroutine,这可以使用runtime包中的Gosched()函数实现。

同步锁

Go语言包中的sync包提供了两种锁类型: sync.Mutex和sync.RWMutex。 Mutex是最简单的一种锁类型,同时也比较暴力,当一个goroutine获得了Mutex后,其他goroutine就只能乖乖等到这个goroutine释放该Mutex。 RWMutex相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用RLock()方法;而写锁(调用Lock()方法)会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占。从RWMutex的实现看, RWMutex类型其实组合了Mutex:
Go并发编程之同步 - 图1

对于这两种锁类型,任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()调用与之对应,否则可能导致等待该锁的所有goroutine处于饥饿状态,甚至可能导致死锁。锁的典型使用模式如下:
Go并发编程之同步 - 图2

全局唯一性操作

对于从全局的角度只需要运行一次的代码,比如全局初始化操作, Go语言提供了一个Once类型来保证全局的唯一性操作,具体代码如下:
Go并发编程之同步 - 图3

如果这段代码没有引入Once, setup()将会被每一个goroutine先调用一次,这至少对于这个例子是多余的。在现实中,我们也经常会遇到这样的情况。 Go语言标准库为我们引入了Once类型以解决这个问题。 once的Do()方法可以保证在全局范围内只调用指定的函数一次(这里指setup()函数),而且所有其他goroutine在调用到此语句时,将会先被阻塞,直至全局唯一的once.Do()调用结束后才继续。

这个机制比较轻巧地解决了使用其他语言时开发者不得不自行设计和实现这种Once效果的难题,也是Go语言为并发性编程做了尽量多考虑的一种体现。

为了更好地控制并行中的原子性操作, sync包中还包含一个atomic子包,它提供了对于一些基础数据类型的原子操作函数,比如下面这个函数:

  1. func CompareAndSwapUint64(val *uint64, old, new uint64) (swapped bool)

就提供了比较和交换两个uint64类型数据的操作。


Go并发编程之同步 - 图4