Merge branch 'master' of github.com:golang-china/gopl-zh

This commit is contained in:
Xargin
2016-01-18 16:14:23 +08:00
91 changed files with 1001 additions and 317 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调用来,而长度从第二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)
@@ -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。

View File

@@ -1,6 +1,6 @@
## 9.2. sync.Mutex互斥鎖
在8.6中,我使用了一buffered channel作为一个计数信号量,来保证最多有20goroutine会同时执行HTTP求。同理,我可以用一容量有1的channel来保证最多有一goroutine在同一时刻访问一个共享量。一个只能为1和0的信量叫做二元信量(binary semaphore)。
在8.6中,我使用了一buffered channel作爲一個計數信號量,來保證最多有20goroutine會同時執行HTTP求。同理,我可以用一容量有1的channel來保證最多有一goroutine在同一時刻訪問一個共享量。一個隻能爲1和0的信量叫做二元信量(binary semaphore)。
```go
gopl.io/ch9/bank2
@@ -23,7 +23,7 @@ func Balance() int {
}
```
这种互斥很而且被sync包里的Mutex型直接支持。它的Lock方法能够获取到token(里叫)且Unlock方法会释放这个token
這種互斥很而且被sync包里的Mutex型直接支持。它的Lock方法能夠獲取到token(里叫)且Unlock方法會釋放這個token
```go
gopl.io/ch9/bank3
@@ -48,14 +48,13 @@ func Balance() int {
}
```
每次一goroutine访问bank变量时(这里只有balance余额变量),它都会调用mutex的Lock方法来获取一互斥。如果其它的goroutine已经获得了这个锁的话,这个操作被阻塞直到其它goroutine用了Unlock使该锁变回可用状态。mutex会保护共享量。惯例来说被mutex所保护的变量是在mutex变量声明之立刻明的。如果你的做法和例不符,保在文档里对你的做法进行说明。
每次一goroutine訪問bank變量時(這里隻有balance餘額變量),它都會調用mutex的Lock方法來獲取一互斥。如果其它的goroutine已經獲得了這個鎖的話,這個操作被阻塞直到其它goroutine調用了Unlock使該鎖變迴可用狀態。mutex會保護共享量。慣例來説被mutex所保護的變量是在mutex變量聲明之立刻明的。如果你的做法和例不符,保在文檔里對你的做法進行説明。
在Lock和Unlock之的代段中的容goroutine可以随便读取或者改,这个代码段叫做临界区。goroutine在结束后释放锁是必要的,无论以哪条路径通过函数都需要放,使是在错误路径中,也要记得释放。
在Lock和Unlock之的代段中的容goroutine可以隨便讀取或者改,這個代碼段叫做臨界區。goroutine在結束後釋放鎖是必要的,無論以哪條路徑通過函數都需要放,使是在錯誤路徑中,也要記得釋放。
上面的bank程序例了一通用的并发模式。一列的出函数封装了一或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一始就取互斥锁并在最后释放锁,从而保共享量不会被并发访问。这种函数、互斥锁和变量的排叫作控monitor(这种老式单词的monitor是受"monitor goroutine"的术语启发而来的。两种用法都是一代理人保证变量被顺序访问)。
由于在存款和查询余额函数中的临界区代码这么短--只有一行,没有分支调用--在代码最后去调用Unlock就显得更为直截了当。在更复杂的临界区的应用中尤其是必须要尽早处理错误并返回的情况下就很难去(靠人)判断对Lock和Unlock的调用是在所有路径中都能够严格配对的了。Go语言里的defer简直就是这种情况下的救星我们用defer来调用Unlock临界区会隐式地延伸到函数作用域的最后这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成这些事情。
上面的bank程序例了一通用的併發模式。一列的出函數封裝了一或多個變量,那麽訪問這些變量唯一的方式就是通過這些函數來做(或者方法,對於一個對象的變量來説)。每一個函數在一始就取互斥鎖併在最後釋放鎖,從而保共享量不會被併發訪問。這種函數、互斥鎖和變量的排叫作控monitor(這種老式單詞的monitor是受"monitor goroutine"的術語啟發而來的。兩種用法都是一代理人保證變量被順序訪問)。
由於在存款和査詢餘額函數中的臨界區代碼這麽短--隻有一行,沒有分支調用--在代碼最後去調用Unlock就顯得更爲直截了當。在更複雜的臨界區的應用中尤其是必須要盡早處理錯誤併返迴的情況下就很難去(靠人)判斷對Lock和Unlock的調用是在所有路徑中都能夠嚴格配對的了。Go語言里的defer簡直就是這種情況下的救星我們用defer來調用Unlock臨界區會隱式地延伸到函數作用域的最後這樣我們就從“總要記得在函數返迴之後或者發生錯誤返迴時要記得調用一次Unlock”這種狀態中獲得了解放。Go會自動幫我們完成這些事情。
```go
func Balance() int {
@@ -65,11 +64,11 @@ func Balance() int {
}
```
上面的例子里Unlock在return语句读取完balance的值之后执所以Balance函数是并发安全的。这带来的另一点好处是,我再也不需要一本地量b了。
上面的例子里Unlock在return語句讀取完balance的值之後執所以Balance函數是併發安全的。這帶來的另一點好處是,我再也不需要一本地量b了。
此外,一deferred Unlock使在临界区发生panic依然会执行,这对于用recover (§5.10)来恢复的程序来说是很重要的。defer调用只会比显式地用Unlock成本高那么一点点,不过却在很大程度上保了代的整性。大多数情况下对于并发程序来说,代的整性比度的化更重要。如果可能的话尽量使用defer来将临界区扩展到函数的结束。
此外,一deferred Unlock使在臨界區發生panic依然會執行,這對於用recover (§5.10)來恢複的程序來説是很重要的。defer調用隻會比顯式地調用Unlock成本高那麽一點點,不過卻在很大程度上保了代的整性。大多數情況下對於併發程序來説,代的整性比度的化更重要。如果可能的話盡量使用defer來將臨界區擴展到函數的結束。
一下下面的Withdraw函。成功的候,它会正确地减掉余额并返回true。但如果银行记录资金对交易来说不足,那取款就会恢复余额,并返回false。
一下下面的Withdraw函。成功的候,它會正確地減掉餘額併返迴true。但如果銀行記録資金對交易來説不足,那取款就會恢複餘額,併返迴false。
```go
// NOTE: not atomic!
@@ -83,9 +82,9 @@ func Withdraw(amount int) bool {
}
```
数终于给出了正确的结果,但是有一点讨厌的副作用。当过多的取款操作同时执行时balance可能会瞬时被减到0以下。可能引起一个并发的取款被不合逻辑地拒。所以如果Bob尝试买一辆sports carAlice可能就没办法为她的早咖啡付款了。里的问题是取款不是一原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥,但任何一次都不会锁上整取款流程。
數終於給出了正確的結果,但是有一點討厭的副作用。當過多的取款操作同時執行時balance可能會瞬時被減到0以下。可能引起一個併發的取款被不合邏輯地拒。所以如果Bob嚐試買一輛sports carAlice可能就沒辦法爲她的早咖啡付款了。里的問題是取款不是一原子操作:它包含了三個步驟,每一步都需要去獲取併釋放互斥,但任何一次都不會鎖上整取款流程。
理想情下,取款应该只在整操作中得一次互斥。下面这样的尝试是错误的:
理想情下,取款應該隻在整操作中得一次互斥。下面這樣的嚐試是錯誤的:
```go
// NOTE: incorrect!
@@ -101,11 +100,11 @@ func Withdraw(amount int) bool {
}
```
上面这个例子中Deposit会调用mu.Lock()第二次去取互斥,但因mutex已经锁上了,而法被重入(译注go里有重入锁,关于重入的概念,请参考java)--也就是说没法对一个已经锁上的mutex再次上--这会导致程序死锁,没法继续执行下去Withdraw会永远阻塞下去。
上面這個例子中Deposit會調用mu.Lock()第二次去取互斥,但因mutex已經鎖上了,而法被重入(譯註go里有重入鎖,關於重入的概念,請參考java)--也就是説沒法對一個已經鎖上的mutex再次上--這會導致程序死鎖,沒法繼續執行下去Withdraw會永遠阻塞下去。
关于Go的互斥量不能重入这一点我们有很充分的理由。互斥量的目的是为了确保共享量在程序执行时的关键点上能够保证不变性。不性的其中之一是“有goroutine访问共享量”。但实际上对于mutex保护的变量来说,不变性还包括其它方面。当一个goroutine得了一互斥锁时,它会断定这种不变性能被保持。其获取并持有锁期间,可能去更新共享量,这样不变性只是短地被破。然而当其释放锁之后,它必须保证不变性已经恢复原样。尽管一可以重入的mutex也可以保证没有其它的goroutine在访问共享量,但这种方式法保证这些变量额外的不性。(译注:这段翻译有点晕)
關於Go的互斥量不能重入這一點我們有很充分的理由。互斥量的目的是爲了確保共享量在程序執行時的關鍵點上能夠保證不變性。不性的其中之一是“有goroutine訪問共享量”。但實際上對於mutex保護的變量來説,不變性還包括其它方面。當一個goroutine得了一互斥鎖時,它會斷定這種不變性能被保持。其獲取併保持鎖期間,可能去更新共享量,這樣不變性隻是短地被破。然而當其釋放鎖之後,它必須保證不變性已經恢複原樣。盡管一可以重入的mutex也可以保證沒有其它的goroutine在訪問共享量,但這種方式法保證這些變量額外的不性。(譯註:這段翻譯有點暈)
通用的解方案是将一个函数分离为多个函数,比如我把Deposit分离成两个:一个不导出的函deposit这个函数假设锁总是会被持有并去做实际的操作,另一个是导出的函Deposit这个函数会调用deposit但在用前先去获取锁。同理我可以Withdraw也表示成这种形式:
通用的解方案是將一個函數分離爲多個函數,比如我把Deposit分離成兩個:一個不導出的函deposit這個函數假設鎖總是會被保持併去做實際的操作,另一個是導出的函Deposit這個函數會調用deposit但在調用前先去獲取鎖。同理我可以Withdraw也表示成這種形式:
```go
func Withdraw(amount int) bool {
@@ -135,9 +134,6 @@ func Balance() int {
func deposit(amount int) { balance += amount }
```
當然這里的存款deposit函數很小實際上取款withdraw函數不需要理會對它的調用盡管如此這里的表達還是表明了規則。
当然这里的存款deposit函数很小实际上取款withdraw函数不需要理会对它的调用尽管如此这里的表达还是表明了规则
封装(§6.6), 用限制一个程序中的意外交互的方式可以使我们获得数据结构的不变性。因为某种原因封装还帮我们获得了并发的不变性。当你使用mutex时确保mutex和其保护的变量没有被导出(在go里也就是小写且不要被大写字母开头的函数访问啦)无论这些变量是包级的变量还是一个struct的字段。
封裝(§6.6), 用限製一個程序中的意外交互的方式可以使我們獲得數據結構的不變性。因爲某種原因封裝還幫我們獲得了併發的不變性。當你使用mutex時確保mutex和其保護的變量沒有被導出(在go里也就是小寫且不要被大寫字母開頭的函數訪問啦)無論這些變量是包級的變量還是一個struct的字段

View File

@@ -20,5 +20,5 @@ Balance函數現在調用了RLock和RUnlock方法來獲取和釋放一個讀取
RLock隻能在臨界區共享變量沒有任何寫入操作時可用。一般來説我們不應該假設邏輯上的隻讀函數/方法也不會去更新某一些變量。比如一個方法功能是訪問一個變量,但它也有可能會同時去給一個內部的計數器+1(譯註:可能是記録這個方法的訪問次數啥的),或者去更新緩存--使卽時的調用能夠更快。如果有疑惑的話,請使用互斥鎖。
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作而鎖在競爭條件下也就是説goroutine們必等待才能獲取到鎖的時候RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録所以會讓它比一般的無競爭鎖的mutex慢一些。
RWMutex隻有當獲得鎖的大部分goroutine都是讀操作而鎖在競爭條件下也就是説goroutine們必等待才能獲取到鎖的時候RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録所以會讓它比一般的無競爭鎖的mutex慢一些。

View File

@@ -87,7 +87,7 @@ func Icon(name string) image.Image {
```
上面的代碼有兩個臨界區。goroutine首先會獲取一個寫鎖査詢map然後釋放鎖。如果條目被找到了(一般情況下)那麽會直接返迴。如果沒有找到那goroutine會獲取一個寫鎖。不釋放共享鎖的話也沒有任何辦法來將一個共享鎖陞級爲一個互斥鎖所以我們必重新檢査icons變量是否爲nil以防止在執行這一段代碼的時候icons變量已經被其它gorouine初始化過了。
上面的代碼有兩個臨界區。goroutine首先會獲取一個寫鎖査詢map然後釋放鎖。如果條目被找到了(一般情況下)那麽會直接返迴。如果沒有找到那goroutine會獲取一個寫鎖。不釋放共享鎖的話也沒有任何辦法來將一個共享鎖陞級爲一個互斥鎖所以我們必重新檢査icons變量是否爲nil以防止在執行這一段代碼的時候icons變量已經被其它gorouine初始化過了。
上面的模闆使我們的程序能夠更好的併發但是有一點太複雜且容易出錯。幸運的是sync包爲我們提供了一個專門的方案來解決這種一次性初始化的問題sync.Once。概念上來講一次性的初始化需要一個互斥量mutex和一個boolean變量來記録初始化是不是已經完成了互斥量用來保護boolean變量和客戶端數據結構。Do這個唯一的方法需要接收初始化函數作爲其參數。讓我們用sync.Once來簡化前面的Icon函數吧

View File

@@ -18,6 +18,6 @@ $ GOMAXPROCS=2 go run hacker-cliché.go
010101010101010101011001100101011010010100110...
```
在第一次執行時最多同時隻能有一個goroutine被執行。初始情況下隻有main goroutine被執行所以會打印很多1。過了一段時間後GO調度器會將其置爲休眠併喚醒另一個goroutine這時候就開始打印很多0了在打印的時候goroutine是被調度到操作繫統線程上的。在第二次執行時我們使用了兩個操作繫統線程所以兩個goroutine可以一起被執行以同樣的頻率交替打印0和1。我們必強調的是goroutine的調度是受很多因子影響的而runtime也是在不斷地發展演進的所以這里的你實際得到的結果可能會因爲版本的不同而與我們運行的結果有所不同。
在第一次執行時最多同時隻能有一個goroutine被執行。初始情況下隻有main goroutine被執行所以會打印很多1。過了一段時間後GO調度器會將其置爲休眠併喚醒另一個goroutine這時候就開始打印很多0了在打印的時候goroutine是被調度到操作繫統線程上的。在第二次執行時我們使用了兩個操作繫統線程所以兩個goroutine可以一起被執行以同樣的頻率交替打印0和1。我們必強調的是goroutine的調度是受很多因子影響的而runtime也是在不斷地發展演進的所以這里的你實際得到的結果可能會因爲版本的不同而與我們運行的結果有所不同。
練習9.6: 測試一下計算密集型的併發程序(練習8.5那樣的)會被GOMAXPROCS怎樣影響到。在你的電腦上最佳的值是多少你的電腦CPU有多少個核心

View File

@@ -1,5 +1,5 @@
# 第九章 基於共享變量的併發
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必處理的一些重要而且細微的問題。
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必處理的一些重要而且細微的問題。
在本章中我們會細致地了解併發機製。尤其是在多goroutine之間的共享變量併發問題的分析手段以及解決這些問題的基本模式。最後我們會解釋goroutine和操作繫統線程之間的技術上的一些區别。