并发 – 同步

  之前喊过一句口号,倡导用通信来共享数据,而不是通过共享数据来进行通信,但考虑到即使成功地用 channel 来作为通信手段,还是避免不了多个 goroutine 之间共享数据的问题,Go 语言的设计者虽然对 channel 有极高的期望,但也提供了妥善的资源锁方案。

同步锁

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

type RWMutex struct {
    w Mutex
    writerSem uint32
    readerSem uint32
    readerCount int32
    readerWait int32
}

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

var l sync.Mutex

func foo() {
    l.Lock()
    defer l.Unlock()
    //...
}

  这里我们再一次见证了 Go 语言 defer 关键字带来的优雅。

全局唯一性操作

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

var a string

var once sync.Once

func setup() {
    a = "hello, world"
}

func doprint() {
    once.Do(setup)
    print(a)
}

func twoprint() {
    go doprint()
    go doprint()
}

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

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

  如果没有 once.Do(),我们很可能只能添加一个全局的 bool 变量,在函数 setup() 的最后一行将该 bool 变量设置为 true。在对 setup() 的所有调用之前,需要先判断该 bool 变量是否已经被设置为 true,如果该值仍然是 false,则调用一次 setup(),否则应跳过该语句。实现代码如下所示:

var done bool = false

func setup() {
    a = "hello, world"
    done = true
}

func doprint() {
    if !done {
        setup()
    }
    print(a)
}

  这段代码初看起来比较合理,但是细看还是会有问题,因为 setup() 并不是一个原子性操作,这种写法可能导致 setup() 函数被多次调用,从而无法达到全局只执行一次的目标。这个问题的复杂性也更加体现了 Once 类型的价值。

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

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

  就提供了比较和交换两个 uint64 类型数据的操作。这让开发者无需再为这样的操作专门添加 Lock 操作。

发表评论

电子邮件地址不会被公开。 必填项已用*标注