同步

创建一个协程是微不足道的, 它们开销很小我们可以启动很多; 但是,需要协调并发代码。为了解决这个问题, Go 提供了 通道。 在我们学习 通道 之前,我认为了解并发编程的基础知识非常重要。

编写并发代码要求您特别注意在哪里读取和写入一个值。 在某些方面, 例如没有垃圾回收的语言 — 它需要您从一个新的角度去考虑您的数据,始终警惕着可能存在的危险。 例如:

package main

import (
  "fmt"
  "time"
)

var counter = 0

func main() {
  for i := 0; i < 20; i++ {
    go incr()
  }
  time.Sleep(time.Millisecond * 10)
}

func incr() {
  counter++
  fmt.Println(counter)
}

你觉得将会输出什么呢?

如果你认为输出的是 1, 2, ... 20 这既不对也没错。如果你运行了以上的代码确实可能得到这个输出。可是,这个操作就很让人懵逼的。 啥?因为我们可能有多个 (这个情况下两个) 协程 同时写入一个相同变量 counter 。或者,同样糟糕的是,一个协程要读取 counter 时,另一个协程正在写入。

这个真的很危险吗?当然啦! counter++ 看起来可能是一行很简单的代码,但它是实际上被拆分为多个汇编语句 — 确切的性质依赖于你跑程序的平台。如果你运行这个例子,你将经常看到那些数字是以一种乱七八糟的顺序打印的,亦或数字是重复的/丢失的。别着急还会有更糟糕的情况, 比方说系统崩溃或者访问并增加任意区块的数据!

从变量中读取变量是唯一安全的并发处理变量的方式。 你可以有想要多少就多少的读取者, 但是写操作必须要得同步。 有太多的方法可以做到这个了,包括使用一些依赖于特殊的 CPU 指令集的真原子操作。然而, 常用的操作还是使用互斥量(译者注:mutex):

package main

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

var (
  counter = 0
  lock sync.Mutex
)

func main() {
  for i := 0; i < 20; i++ {
    go incr()
  }
  time.Sleep(time.Millisecond * 10)
}

func incr() {
  lock.Lock()
  defer lock.Unlock()
  counter++
  fmt.Println(counter)
}

互斥量序列化会锁住锁下的代码访问。因为默认的的 sync.Mutex 是未锁定状态,这儿我们就得先定义 lock sync.Mutex

这操作是不看着超简单? 这个例子是具有欺骗性的。当我们进行并发编程时会产生一系列严重的 Bug。 首先,并不是经常能很明显知道什么代码需要保护。使用这样粗糙的锁操作(覆盖着大量代码的锁操作)确实很诱人,这就违背了我们当初进行并发编程的初心了。 我们肯定是需要个优雅的锁操作; 否则,我们最终会把多条快速通道走成单车道的。

另外一个问题是与死锁有关。 使用单个锁时,这没有问题,但是如果你在代码中使用两个或者更多的锁,很容易出现一种危险的情况,当协程A拥有锁 lockA ,想去访问锁 lockB ,同时协程B拥有锁 lockB 并需要访问锁 lockA

实际上我们使用一个锁时也有可能发生死锁的问题,就是当我们忘记释放它时。 但是这和多个锁引起的死锁行为相比起来,这并不像多锁死锁那样危险(因为这真的 很难发现),当你试着运行下面的代码时,您可以看见发生了什么:

package main

import (
  "time"
  "sync"
)

var (
  lock sync.Mutex
)

func main() {
  go func() { lock.Lock() }()
  time.Sleep(time.Millisecond * 10)
  lock.Lock()
}

到现在为止还有很多并发编程我们没有看到过。 首先,有一个常见的锁叫读写互斥锁。它主要提供了两种锁功能: 一个锁定读取和一个锁定写入。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中, sync.RWMutex 就是这种锁。另外 sync.Mutex 结构不但提供了LockUnlock 方法 ,也提供了RLockRUnlock 方法;其中 R 代表 Read.。虽然读写锁很常用,它也给开发人员带来了额外的负担:我们不但要关注我们正在访问的数据,还要注意如何访问。

此外,部分并发编程不只是通过为数不多的代码按顺序的访问变量; 它也需要协调多个协程。 例如,休眠10毫秒并不是一个特别优雅的解决方案。如果一个协程消耗的时间需要超过10毫秒怎么办?如果协程消耗更少的时间而我们浪费周期怎么办?又或者可以等待协程运行完毕, 我们想另外一个协程 嗨, 我有新的数据需要你处理?

这些事在没有 通道 的情况下都是可以完成的。当然对于更简单的情况,我相信你 应该 使用基本的功能比如 sync.Mutexsync.RWMutex, 但正如我们将会在下一节中看到的那样, 通道 旨在让并发编程更简洁和不容易出错。

热门教程

最新教程