创建一个协程没有什么难度,并且启动很多协程开销也不大。但是,并发执行的代码需要协同。为了帮助我们解决这个问题,go提供了通道(channels)。在学习通道之前,我认为有必要先学习了并发编程的基本知识。
在编写并发执行的代码时,你需要特别的关注在哪里和如何读写一个值。出于某些原因,例如没有垃圾回收的语言,需要你从一个新的角度去考虑你的数据,总是警惕着可能存在的危险。例如:
package main import ( "fmt" "time" ) var counter = 0 func main() { for i := 0; i < 2; i++ { go incr() } time.Sleep(time.Millisecond * 10) } func incr() { counter++ fmt.Println(counter) }
你认为会输出什么?
如果你觉得输出是1和2,不能说你对或者错。如果你运行上面的代码,确实如此。你很有可能得到那样的输出。但是,实际上这个输出是不确定的。为什么?因为我们可能有多个(这个例子中是2个)go协程同时写同一个变量counter。或者更糟的情况是一个协程正在读counter,而另一个协程正在写counter。
1
2
counter
这确实危险吗?绝对是的。counter++似乎看起来只是一行简单的代码,但是实际上它被拆分为很多汇编指令,具体依赖于你运行的软件和硬件平台。在上面的例子中,确实在大多数情况下运行良好。然而,另外一个可能的结果是counter等于0 时被2个协程同时读取,那么你将得到一个输出是1,1。还有更坏的结果,例如系统崩溃或者得到一个任意值然后自增。
counter++
1,1
在并发程序中,如果想安全的操作一个变量,唯一的手段就是读取该变量。你可以有任意多的程序去读,但是写必须是同步的。这里有几种方式实现,包括使用依赖于特殊cpu架构的一些真正的原子操作。然而,大多数时候都是使用一个互斥锁:
package main import ( "fmt" "sync" "time" ) var ( counter = 0 lock sync.Mutex ) func main() { for i := 0; i < 2; i++ { go incr() } time.Sleep(time.Millisecond * 10) } func incr() { lock.Lock() defer lock.Unlock() counter++ fmt.Println(counter) }
互斥锁可以使你按顺序访问代码。因为sync.Mutex默认值是没有锁的,所以我们简单的定义了一个锁lock sync.Mutex。
sync.Mutex
lock sync.Mutex
看起来似乎很简单?上面的例子带有欺骗性。当做并发编程时会发现一些列很严重的bug。首先,那些代码需要被保护一直都不是容易发现。虽然它可能是想使用一个低级锁(这个锁涉及了很多代码),这些潜在出错的地方是我们做并发编程首先要去考虑的。我们常常想要精确的锁,或者我们最终由一个10车道的高速突然转变成一个单车道道路。
另外一个问题是如何处理死锁。当使用一个锁时,这没有问题,但是如果你在代码中使用2个或者更多的锁,很容易出现一种危险的情况,即协程A拥有锁lockA,想去访问锁lockB,同时协程B拥有lockB并需要访问锁lockA。
lockA
lockB
实际上使用一个锁也有可能发生死锁问题,即当我们忘记释放它时。但是这和多个锁引起的死锁为比起来,危害性不大(因为这真的很难发现),但是当你试着运行下面代码时,你可以看见发生了什么:
package main import ( "sync" "time" ) var ( lock sync.Mutex ) func main() { go func() { lock.Lock() }() time.Sleep(time.Millisecond * 10) lock.Lock() }
迄今为止有很多并发编程我们都还没用见过。首先,由于我们可以同时有多个读操作,有一种常见的锁叫读写锁。它主要提供2中锁功能:一个锁定读和一个锁定写。在go语言中,sync.RWMutex就是这种锁。另外sync.Mutex结构不但提供了Lock和Unlock方法,也提供了RLock和RLock方法,这里的R代表Read。虽然读写锁很常用,但是他们也给开发者带来一些额外的负担:我们不但要关注我们正在访问的数据,而且也要关注如何访问。
sync.RWMutex
Lock
Unlock
RLock
R
Read
此外,部分并发编程不只是通过为数不多代码按顺序的访问变量,也需要协调多个go协程。例如,休眠10毫秒不是一种优雅的方法。如果一个go协程消耗的时间不止10毫秒呢?如果go协程消耗少于10毫秒,我们只是浪费了cpu?又或者可以等待go协程运行完毕,我们告诉另外一个go协程:嗨,我有一些新数据给你处理?
所有的这些事在没有通道(channels)的情况下都是可以实现的。当然,对于更简单的例子,我认为你应该使用基本的功能例如sync.Mutex和sync.RWMutex。但是在下一节我们将看到,通道的主要目的是为了使并发编程更简洁和不易出错。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8