内容简介:本系列是阅读 “The Go Programming Language” 理解和记录。在 Go 的程序中,如果只有一个 goroutine 也就是只有一个 main goroutine 存在时,所有代码都是顺序执行的,也就是说程序的执行步骤就是它们的逻辑顺序,一个步骤是否能在另一个步骤之前或者之后发生时非常确定的。然而如果一个 Go 程序有两个或者多个 goroutine 存在,我们就很难确定一个 event(事件) x 是否发生在另一个 goroutine 中的 event(事件) y 之前,之后,还是
本系列是阅读 “The Go Programming Language” 理解和记录。
在 Go 的程序中,如果只有一个 goroutine 也就是只有一个 main goroutine 存在时,所有代码都是顺序执行的,也就是说程序的执行步骤就是它们的逻辑顺序,一个步骤是否能在另一个步骤之前或者之后发生时非常确定的。然而如果一个 Go 程序有两个或者多个 goroutine 存在,我们就很难确定一个 event(事件) x 是否发生在另一个 goroutine 中的 event(事件) y 之前,之后,还是同时发生。如果我们不能确定 x 和 y 的发生顺序,则说 x 和 y 是并发的 concurrent。
考虑一个概念 concurrency safe ,这个概念是指一个在顺序执行的程序中能够正常执行的 function 如果可以并发的执行,并且在不采用任何同步措施的情况下执行结果依然正确,那么这个 function 是 concurrency safe 的,也就是说它可以并发的执行。同理,如果一个 type 的所有方法以及对它自身的所有操作是 concurrency safe 的,则这个 type 是 concurrency safe 的。
我们在编写一个 Go 程序时并不需要保证所有的使用的 type 都是 concurrency safe 的,只在需要对某个 type 进行并发操作的情况下保证 type 是 concurrency safe 的,大多数情况下我们应该尽量保证对程序 variable 的访问都尽量在单个 goroutine 完成或者使用互斥锁来保护我们需要访问的程序区域。
在继续探讨我们的话题之前,我们必须要知道 Go 中 race condition 就是说程序在多个 goroutine 交互执行的情况下可能会给出不正确的结果。理解 race condition 讲的是什么,是理解并发程序的关键所在。
下面我们用一个简单的例子来说明 race condition 可能会带来的问题,这个例子展示了读取账户余额和向账户中存钱的这两个操作。
// Package bank implements a bank with only one account.
package bank
var balance int
func Deposit(amountint) { balance = balance + amount }
func Balance()int { return balance }
如果这两个操作是以下面的形式发生在两个 goroutine 中:
// Alice:
go func() {
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
}()
// Bob:
go bank.Deposit(100) // B
最终结果可能会有以下几种形式:
| Alice first | Bob first | Alice /Bob /Alice |
|---|---|---|
| 0 | 0 | 0 |
| A1 200 | B 100 | A1 200 |
| A2 “= 200” | A1 300 | B 300 |
| B 300 | A2 “= 300” | A2 “= 300” |
在这几种结果中最终的 balance 都是 300,差异仅仅是在 Alice 看到的 balance 是 200 还是 300 的差异,看起来并没有造成太大的影响,在此我们首先来解释一下为什么会有这样的差异。
在现代计算机结构中,多核 CPU 对内存的操作并不是完全实时的,每个 CPU 为了能够加速对内存的访问都会有 cache,CPU 把内存的数据读出来之后,进行修改然后重新写入内存的过程中,可能其它的 CPU 已经对内存区域进行修改了,这就是多核 CPU 在对 shared data 进行存取时引入的 race condition。为了能够防止这种情况的发生,很多 CPU 指令都提供了原子操作来保证对内存的修改是唯一且正确的。
在上面的例子中,由于两个 goroutine 对 balance 这个同一内存区域读取的时机差异,就有可能会造成不同的执行顺序输出的结果是不同的。
而下面这种执行顺序造成的影响在现实世界中是不能容忍的。
| action | balance | |
|---|---|---|
| A1r | 0 | balance + amount |
| B | 100 | balance = balance + amount |
| A1w | 200 | balance = … |
| A2 | 200 | Println(“= “, balance) |
最终 B 操作的 100 消失了!这样的程序我们称之为 race condition 中的 data race ,用一句话概括其含义就是: Data race 经常发生在多个 goroutine 存取同一个 variable 时,至少有一个 goroutine 会执行修改操作 。
如何避免 data race?
第一种方式是 避免对共享 variable 进行写操作
Go 中 map 不是 concurrency safe,如果在多个 goroutine 中不加锁而直接对 map 操作就很难避免错误的结果出现:
var icons = make(map[string]image.Image)
func loadIcon(namestring)image.Image
// NOTE: not concurrency-safe!
func Icon(namestring) image.Image
{
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
}
上面的例子中对 icons 的操作不是 concurrency safe。
第二种方式是 避免在多个 goroutine 中存取变量
Go 中这样的操作很常见,为了保证数据的准确性,只在一个 goroutine 中执行对 variable 的所有操作,其他 goroutine 通过 channel 向该 goroutine 进行请求来实现对 variable 的存取和更新,这样保证了 variable 的存取都只在一个 goroutine 中进行,这也充分对 Go 的理念进行阐释: Do not communicate by sharing memory; instead, share memory by communicating 。
第三种方式是 使用锁来保护共享的 variable 来保证任意时刻只有一个 goroutine 对变量进行修改
关于锁的使用,我们会在下一节进行详细介绍。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Masterminds of Programming
Federico Biancuzzi、Chromatic / O'Reilly Media / 2009-03-27 / USD 39.99
Description Masterminds of Programming features exclusive interviews with the creators of several historic and highly influential programming languages. Think along with Adin D. Falkoff (APL), Jame......一起来看看 《Masterminds of Programming》 这本书的介绍吧!