make loop

pull/1/head
chai2010 2016-01-13 16:56:39 +08:00
parent 81a9ebf4de
commit 49388170e2
1 changed files with 58 additions and 58 deletions

View File

@ -1,18 +1,18 @@
## 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,22 +22,22 @@ 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:
go func() {
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
bank.Deposit(200) // A1
fmt.Println("=", bank.Balance()) // A2
}()
// Bob:
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调用来而长度从第二个make来x就变成了一个混合体一个自称长度为1,000,000但实际上内部只有10个元素的slice。这样导致的结果是存储999,999元素的位置会碰撞一个遥远的内存位置这种情况下难以对值进行预测而且定位和debug也会变成噩梦。这种语义雷区被称为未定义行为对C程序员来说应该很熟悉幸运的是在Go语言里造成的麻烦要比C里小得多。
後一個語句中的x的值是未定義的其可能是nil或者也可能是一個長度爲10的slice也可能是一個程度爲1,000,000的slice。但是迴憶一下slice的三個組成部分指針(pointer)、長度(length)和容量(capacity)。如果指針是從第一個make調用來而長度從第二個make來x就變成了一個混合體一個自稱長度爲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)
@ -87,36 +87,36 @@ func loadIcon(name string) image.Image
// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
icon, ok := icons[name]
if !ok {
icon = loadIcon(name)
icons[name] = icon
}
return icon
}
```
反之,如果我们在创建goroutine之前的初始化阶段就初始化了map中的所有条目并且再也不去修改它们那么任意数量的goroutine并发访问Icon都是安全的因为每一个goroutine都只是去读取而已。
反之,如果我們在創建goroutine之前的初始化階段就初始化了map中的所有條目併且再也不去脩改它們那麽任意數量的goroutine併發訪問Icon都是安全的因爲每一個goroutine都隻是去讀取而已。
```go
var icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}
// Concurrency-safe.
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
```go
gopl.io/ch9/bank1
@ -130,45 +130,45 @@ func Deposit(amount int) { deposits <- amount }
func Balance() int { return <-balances }
func teller() {
var balance int // balance is confined to teller goroutine
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
var balance int // balance is confined to teller goroutine
for {
select {
case amount := <-deposits:
balance += amount
case balances <- balance:
}
}
}
func init() {
go teller() // start the monitor goroutine
go teller() // start the monitor goroutine
}
```
即使当一个变量无法在其整个生命周期内被绑定到一个独立的goroutine绑定依然是并发问题的一个解决方案。例如在一条流水线上的goroutine之间共享变量是很普遍的行为在这两者间会通过channel来传输地址信息。如果流水线的每一个阶段都能够避免在将变量传送到下一阶段时再去访问它那么对这个变量的所有访问就是线性的。其效果是变量会被绑定到流水线的一个阶段传送完之后被绑定到下一个以此类推。这种规则有时被称为串行绑定。
卽使當一個變量無法在其整個生命週期內被綁定到一個獨立的goroutine綁定依然是併發問題的一個解決方案。例如在一條流水線上的goroutine之間共享變量是很普遍的行爲在這兩者間會通過channel來傳輸地址信息。如果流水線的每一個階段都能夠避免在將變量傳送到下一階段時再去訪問它那麽對這個變量的所有訪問就是線性的。其效果是變量會被綁定到流水線的一個階段傳送完之後被綁定到下一個以此類推。這種規則有時被稱爲串行綁定。
下面的例子中Cakes会被严格地顺序访问先是baker gorouine然后是icer gorouine
下面的例子中Cakes會被嚴格地順序訪問先是baker gorouine然後是icer gorouine
```go
type Cake struct{ state string }
func baker(cooked chan<- *Cake) {
for {
cake := new(Cake)
cake.state = "cooked"
cooked <- cake // baker never touches this cake again
}
for {
cake := new(Cake)
cake.state = "cooked"
cooked <- cake // baker never touches this cake again
}
}
func icer(iced chan<- *Cake, cooked <-chan *Cake) {
for cake := range cooked {
cake.state = "iced"
iced <- cake // icer never touches this cake again
}
for cake := range cooked {
cake.state = "iced"
iced <- cake // icer never touches this cake again
}
}
```
第三种避免数据竞争的方法是允许很多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。