回到简体

This commit is contained in:
chai2010
2016-02-15 11:06:34 +08:00
parent 9e878f9944
commit 2b37b23285
177 changed files with 2354 additions and 2354 deletions

View File

@@ -1,18 +1,18 @@
## 9.1. 競爭條
## 9.1. 竞争条
在一個線性(就是説隻有一goroutine的)的程序中,程序的執行順序隻由程序的邏輯來決定。例如,我有一段句序列,第一在第二之前(廢話),以此推。在有兩個或更多goroutine的程序中每一goroutine內的語句也是按照定的序去行的,但是一般情下我們沒法去知道分别位於兩個goroutine的事件x和y的執行順x是在y之前是之後還是同時發生是法判的。當我們能夠沒有辦法自信地確認一個事件是在另一事件的前面或者後面發生的,就明x和y這兩個事件是併發的。
在一个线性(就是说只有一goroutine的)的程序中,程序的执行顺序只由程序的逻辑来决定。例如,我有一段句序列,第一在第二之前(废话),以此推。在有两个或更多goroutine的程序中每一goroutine内的语句也是按照定的序去行的,但是一般情下我们没法去知道分别位于两个goroutine的事件x和y的执行顺x是在y之前是之后还是同时发生是法判的。当我们能够没有办法自信地确认一个事件是在另一事件的前面或者后面发生的,就明x和y这两个事件是并发的。
一下,一個函數在線性程序中可以正地工作。如果在併發的情下,這個函數依然可以正地工作的,那麽我們就説這個函數是併發安全的,併發安全的函不需要外的同步工作。我可以把這個概念概括爲一個特定型的一些方法和操作函,如果這個類型是併發安全的,那所有它的訪問方法和操作就都是併發安全的。
一下,一个函数在线性程序中可以正地工作。如果在并发的情下,这个函数依然可以正地工作的,那么我们就说这个函数是并发安全的,并发安全的函不需要外的同步工作。我可以把这个概念概括为一个特定型的一些方法和操作函,如果这个类型是并发安全的,那所有它的访问方法和操作就都是并发安全的。
在一程序中有非併發安全的型的情下,我依然可以使這個程序併發安全。確實,併發安全的型是例外,而不是規則,所以隻有當文檔中明確地説明了其是併發安全的情下,你才可以併發地去訪問它。我們會避免併發訪問大多數的類型,無論是將變量局限在一的一goroutine內還是用互斥條件維持更高别的不性都是爲了這個目的。我們會在本章中説明這些術語
在一程序中有非并发安全的型的情下,我依然可以使这个程序并发安全。确实,并发安全的型是例外,而不是规则,所以只有当文档中明确地说明了其是并发安全的情下,你才可以并发地去访问它。我们会避免并发访问大多数的类型,无论是将变量局限在一的一goroutine内还是用互斥条件维持更高别的不性都是为了这个目的。我们会在本章中说明这些术语
相反,出包别的函一般情下都是併發安全的。由package級的變量沒法被限製在單一的gorouine所以脩改這些變量“必”使用互斥件。
相反,出包别的函一般情下都是并发安全的。由package级的变量没法被限制在单一的gorouine所以修改这些变量“必”使用互斥件。
個函數在併發調用時沒法工作的原因太多了,比如死(deadlock)、活(livelock)和死(resource starvation)。我們沒有空去討論所有的問題,這里我們隻聚焦在競爭條件上。
个函数在并发调用时没法工作的原因太多了,比如死(deadlock)、活(livelock)和饿死(resource starvation)。我们没有空去讨论所有的问题,这里我们只聚焦在竞争条件上。
競爭條件指的是程序在多goroutine交叉行操作時,沒有給出正確的結果。競爭條件是很劣的一種場景,因爲這種問題會一直伏在你的程序里,然在非常少見的時候蹦出,或許隻是會在很大的負載時才會發生,又或許是會在使用了某一個編譯器、某一平台或者某一種架構的時候才會出現。這些使得競爭條件帶來的問題非常難以複現而且以分析診斷
竞争条件指的是程序在多goroutine交叉行操作时,没有给出正确的结果。竞争条件是很劣的一种场景,因为这种问题会一直伏在你的程序里,然在非常少见的时候蹦出,或许只是会在很大的负载时才会发生,又或许是会在使用了某一个编译器、某一平台或者某一种架构的时候才会出现。这些使得竞争条件带来的问题非常难以复现而且以分析诊断
傳統上經常用經濟損失來爲競爭條件做比喻,所以我們來看一個簡單的銀行賬戶程序。
传统上经常用经济损失来为竞争条件做比喻,所以我们来看一个简单的银行账户程序。
```go
// Package bank implements a bank with only one account.
@@ -22,9 +22,9 @@ func Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }
```
(然我也可以把Deposit存款函數寫成balance += amount這種形式也是等的,不過長一些的形式解釋起來更方便一些。)
(然我也可以把Deposit存款函数写成balance += amount这种形式也是等的,不过长一些的形式解释起来更方便一些。)
對於這個具體的程序而言,我可以瞅一眼各存款和査餘額的順序調用,都能出正確的結果。也就是Balance函數會給出之前的所有存入的度之和。然而,當我們併發地而不是序地調用這些函數的話Balance就再也沒辦法保證結果正了。考一下下面的兩個goroutine其代表了一個銀行聯合賬戶的兩筆交易:
对于这个具体的程序而言,我可以瞅一眼各存款和查余额的顺序调用,都能出正确的结果。也就是Balance函数会给出之前的所有存入的度之和。然而,当我们并发地而不是序地调用这些函数的话Balance就再也没办法保证结果正了。考一下下面的两个goroutine其代表了一个银行联合账户的两笔交易:
```go
// Alice:
@@ -37,7 +37,7 @@ go func() {
go bank.Deposit(100) // B
```
Alice存了$200後檢査她的餘額,同Bob存了$100。因A1和A2是和B併發執行的,我們沒法預測他們發生的先後順序。直觀地來看的,我們會認爲其執行順序隻有三可能性“Alice先”“Bob先”以及“Alice/Bob/Alice”交錯執行。下面的表格展示經過每一步驟後balance量的值。引里的字符串表示餘額單
Alice存了$200后检查她的余额,同Bob存了$100。因A1和A2是和B并发执行的,我们没法预测他们发生的先后顺序。直观地来看的,我们会认为其执行顺序只有三可能性“Alice先”“Bob先”以及“Alice/Bob/Alice”交错执行。下面的表格展示经过每一步骤后balance量的值。引里的字符串表示余额单
```
Alice first Bob first Alice/Bob/Alice
@@ -47,9 +47,9 @@ A2 "=200" A1 300 B 300
B 300 A2 "=300" A2 "=300"
```
所有情下最終的餘額都是$300。唯一的變數是Alice的餘額單是否包含了Bob交易過無論怎麽着客都不在意。
所有情下最终的余额都是$300。唯一的变数是Alice的余额单是否包含了Bob交易过无论怎么着客都不在意。
但是事是上面的直覺推斷是錯誤的。第四可能的果是事存在的,這種情況下Bob的存款在Alice存款操作中,在餘額被讀到(balance + amount)之,在餘額被更新之前(balance = ...)這樣會導致Bob的交易失。而是因Alice的存款操作A1實際上是兩個操作的一序列,取然後寫;可以稱之爲A1r和A1w。下面是交叉時産生的問題
但是事是上面的直觉推断是错误的。第四可能的果是事存在的,这种情况下Bob的存款在Alice存款操作中,在余额被读到(balance + amount)之,在余额被更新之前(balance = ...)这样会导致Bob的交易失。而是因Alice的存款操作A1实际上是两个操作的一序列,取然后写;可以称之为A1r和A1w。下面是交叉时产生的问题
```
Data race
@@ -60,11 +60,11 @@ A1w 200 balance = ...
A2 "= 200"
```
在A1r之balance + amount會被計算爲200所以是A1w會寫入的值,不受其它存款操作的榦預。最終的餘額是$200。行的賬戶上的資産比Bob實際的資産多了$100。(譯註:因爲丟失了Bob的存款操作所以其實是説Bob的錢丟了)
在A1r之balance + amount会被计算为200所以是A1w会写入的值,不受其它存款操作的干预。最终的余额是$200。行的账户上的资产比Bob实际的资产多了$100。(译注:因为丢失了Bob的存款操作所以其实是说Bob的钱丢了)
這個程序包含了一特定的競爭條件,叫作數據競爭。無論任何候,要有兩個goroutine併發訪問同一量,且至少其中的一個是寫操作的候就會發生數據競爭
这个程序包含了一特定的竞争条件,叫作数据竞争。无论任何候,要有两个goroutine并发访问同一量,且至少其中的一个是写操作的候就会发生数据竞争
如果數據競爭的對象是一比一個機器字(譯註32位器上一字=4個字節)更大的類型時,事情就得更麻比如interfacestring或者slice型都是如此。下面的代碼會併發地更新兩個不同度的slice
如果数据竞争的对象是一比一个机器字(译注32位器上一字=4个字节)更大的类型时,事情就得更麻比如interfacestring或者slice型都是如此。下面的代码会并发地更新两个不同度的slice
```go
var x []int
@@ -73,13 +73,13 @@ go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!
```
後一個語句中的x的值是未定其可能是nil或者也可能是一個長度爲10的slice也可能是一程度1,000,000的slice。但是迴憶一下slice的三個組成部分:指(pointer)、度(length)和容量(capacity)。如果指針是從第一make調用來,而長度從第二makex就成了一混合,一個自稱長度爲1,000,000但實際上內部隻有10元素的slice。這樣導致的果是存999,999元素的位置碰撞一個遙遠的內存位置這種情況下難以對值進行預測而且定位和debug也會變成噩夢。這種語義雷區被稱爲未定義行爲對C程序員來説應該很熟悉;幸的是在Go言里造成的麻要比C里小得多。
后一个语句中的x的值是未定其可能是nil或者也可能是一个长度为10的slice也可能是一程度1,000,000的slice。但是回忆一下slice的三个组成部分:指(pointer)、度(length)和容量(capacity)。如果指针是从第一make调用来,而长度从第二makex就成了一混合,一个自称长度为1,000,000但实际上内部只有10元素的slice。这样导致的果是存999,999元素的位置碰撞一个遥远的内存位置这种情况下难以对值进行预测而且定位和debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉;幸的是在Go言里造成的麻要比C里小得多。
盡管併發程序的概念讓我們知道併發併不是簡單的語句交叉行。我們將會在9.4中看到,數據競爭可能有奇怪的果。多程序,甚至一些非常明的人也還是會偶爾提出一些理由來允許數據競爭,比如:“互斥件代太高”,“這個邏輯隻是用做logging”“我不介意失一些消息”等等。因在他們的編譯器或者平台上很少遇到問題,可能了他們錯誤的信心。一好的經驗法則是根本就有什麽所謂的良性數據競爭。所以我一定要避免數據競爭,那在我的程序中要如何做到呢?
尽管并发程序的概念让我们知道并发并不是简单的语句交叉行。我们将会在9.4中看到,数据竞争可能有奇怪的果。多程序,甚至一些非常明的人也还是会偶尔提出一些理由来允许数据竞争,比如:“互斥件代太高”,“这个逻辑只是用做logging”“我不介意失一些消息”等等。因在他们的编译器或者平台上很少遇到问题,可能了他们错误的信心。一好的经验法则是根本就有什么所谓的良性数据竞争。所以我一定要避免数据竞争,那在我的程序中要如何做到呢?
們來重複一下數據競爭的定,因爲實在太重要了:數據競爭會在兩個以上的goroutine併發訪問相同的量且至少其中一個爲寫操作時發生。根上述定,有三方式可以避免數據競爭
们来重复一下数据竞争的定,因为实在太重要了:数据竞争会在两个以上的goroutine并发访问相同的量且至少其中一个为写操作时发生。根上述定,有三方式可以避免数据竞争
第一方法是不要去寫變量。考一下下面的map被“”填充,也就是在每key被第一次求到的候才去填值。如果Icon是被順序調用的話,這個程序工作很正常但如果Icon被併發調用那麽對於這個map來説就會存在數據競爭
第一方法是不要去写变量。考一下下面的map被“”填充,也就是在每key被第一次求到的候才去填值。如果Icon是被顺序调用的话,这个程序工作很正常但如果Icon被并发调用那么对于这个map来说就会存在数据竞争
```go
var icons = make(map[string]image.Image)
@@ -96,7 +96,7 @@ func Icon(name string) image.Image {
}
```
反之,如果我們在創建goroutine之前的初始化就初始化了map中的所有條目併且再也不去改它,那任意量的goroutine併發訪問Icon都是安全的每一goroutine都是去取而已。
反之,如果我们在创建goroutine之前的初始化就初始化了map中的所有条目并且再也不去改它,那任意量的goroutine并发访问Icon都是安全的每一goroutine都是去取而已。
```go
var icons = map[string]image.Image{
@@ -110,13 +110,13 @@ var icons = map[string]image.Image{
func Icon(name string) image.Image { return icons[name] }
```
上面的例子里icons量在包初始化段就已經被賦值了包的初始化是在程序main函數開始執行之前就完成了的。要初始化完成了icons就再也不會脩改的或者不量是本來就併發安全的,這種變量不需要行同步。不過顯然我們沒法用這種方法,因update操作是必要的操作尤其對於銀行賬戶來説
上面的例子里icons量在包初始化段就已经被赋值了包的初始化是在程序main函数开始执行之前就完成了的。要初始化完成了icons就再也不会修改的或者不量是本来就并发安全的,这种变量不需要行同步。不过显然我们没法用这种方法,因update操作是必要的操作尤其对于银行账户来说
第二避免數據競爭的方法是,避免從多個goroutine訪問變量。也是前一章中大多程序所采用的方法。例如前面的併發web爬(§8.6)的main goroutine是唯一一個能夠訪問seen map的goroutine而聊天服器(§8.10)中的broadcaster goroutine是唯一一個能夠訪問clients map的goroutine。這些變量都被限定在了一個單獨的goroutine中。
第二避免数据竞争的方法是,避免从多个goroutine访问变量。也是前一章中大多程序所采用的方法。例如前面的并发web爬(§8.6)的main goroutine是唯一一个能够访问seen map的goroutine而聊天服器(§8.10)中的broadcaster goroutine是唯一一个能够访问clients map的goroutine。这些变量都被限定在了一个单独的goroutine中。
其它的goroutine不能直接訪問變量,它們隻能使用一channel來發送給指定的goroutine請求來査詢更新量。也就是Go的口頭禪“不要使用共享數據來通信;使用通信共享數據”。一提供對一個指定的量通cahnnel來請求的goroutine叫做這個變量的控(monitor)goroutine。例如broadcaster goroutine會監控(monitor)clients map的全部訪問
其它的goroutine不能直接访问变量,它们只能使用一channel来发送给指定的goroutine请求来查询更新量。也就是Go的口头禅“不要使用共享数据来通信;使用通信共享数据”。一提供对一个指定的量通cahnnel来请求的goroutine叫做这个变量的控(monitor)goroutine。例如broadcaster goroutine会监控(monitor)clients map的全部访问
下面是一個重寫了的行的例子,這個例子中balance量被限在了monitor goroutine中teller
下面是一个重写了的行的例子,这个例子中balance量被限在了monitor goroutine中teller
<u><i>gopl.io/ch9/bank1</i></u>
```go
@@ -145,9 +145,9 @@ func init() {
}
```
卽使當一個變量無法在其整生命週期內被綁定到一個獨立的goroutine定依然是併發問題的一個解決方案。例如在一流水上的goroutine之共享量是很普遍的行,在這兩者間會通過channel來傳輸地址信息。如果流水的每一個階段都能避免在將變量傳送到下一階段時再去訪問它,那麽對這個變量的所有訪問就是性的。其效果是變量會被綁定到流水的一個階段,送完之後被綁定到下一,以此推。這種規則有時被稱爲串行定。
即使当一个变量无法在其整生命周期内被绑定到一个独立的goroutine定依然是并发问题的一个解决方案。例如在一流水线上的goroutine之共享量是很普遍的行,在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能避免在将变量传送到下一阶段时再去访问它,那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段,送完之后被绑定到下一,以此推。这种规则有时被称为串行定。
下面的例子中Cakes會被嚴格地順序訪問先是baker gorouine是icer gorouine
下面的例子中Cakes会被严格地顺序访问先是baker gorouine是icer gorouine
```go
type Cake struct{ state string }
@@ -168,7 +168,7 @@ func icer(iced chan<- *Cake, cooked <-chan *Cake) {
}
```
第三避免數據競爭的方法是允很多goroutine去訪問變量,但是在同一個時刻最多有一goroutine在訪問。這種方式被稱爲“互斥”,在下一節來討論這個主題
第三避免数据竞争的方法是允很多goroutine去访问变量,但是在同一个时刻最多有一goroutine在访问。这种方式被称为“互斥”,在下一节来讨论这个主题
**練習 9.1** gopl.io/ch9/bank1程序添加一Withdraw(amount int)取款函。其返迴結果應該要表明事是成功了是因爲沒有足夠資金失了。這條消息會被發送給monitor的goroutine且消息需要包含取款的度和一新的channel這個新channel被monitor goroutine把boolean結果發迴給Withdraw。
**练习 9.1** gopl.io/ch9/bank1程序添加一Withdraw(amount int)取款函。其返回结果应该要表明事是成功了是因为没有足够资金失了。这条消息会被发送给monitor的goroutine且消息需要包含取款的度和一新的channel这个新channel被monitor goroutine把boolean结果发回给Withdraw。