回到简体

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。

View File

@@ -1,6 +1,6 @@
## 9.2. sync.Mutex互斥
## 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)。
<u><i>gopl.io/ch9/bank2</i></u>
```go
@@ -23,7 +23,7 @@ func Balance() int {
}
```
這種互斥很而且被sync包里的Mutex型直接支持。它的Lock方法能夠獲取到token(里叫)且Unlock方法會釋放這個token
这种互斥很而且被sync包里的Mutex型直接支持。它的Lock方法能够获取到token(里叫)且Unlock方法会释放这个token
<u><i>gopl.io/ch9/bank3</i></u>
```go
@@ -48,13 +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"的術語啟發而來的。兩種用法都是一代理人保證變量被順序訪問)。
上面的bank程序例了一通用的并发模式。一列的出函数封装了一或多个变量,那么访问这些变量唯一的方式就是通过这些函数来做(或者方法,对于一个对象的变量来说)。每一个函数在一始就取互斥锁并在最后释放锁,从而保共享量不会被并发访问。这种函数、互斥锁和变量的排叫作控monitor(这种老式单词的monitor是受"monitor goroutine"的术语启发而来的。两种用法都是一代理人保证变量被顺序访问)。
在存款和査詢餘額函數中的臨界區代碼這麽短--有一行,有分支調用--在代碼最後去調用Unlock就得更直截了。在更複雜的臨界區的應用中,尤其是必須要盡早處理錯誤併返迴的情下,就很去(靠人)判斷對Lock和Unlock的調用是在所有路中都能夠嚴格配的了。Go言里的defer直就是這種情況下的救星:我用defer來調用Unlock臨界區會隱式地延伸到函作用域的最後,這樣我們就從“總要記得在函數返迴之後或者發生錯誤返迴時要記得調用一次Unlock”這種狀態中獲得了解放。Go會自動幫我們完成些事情。
在存款和查询余额函数中的临界区代码这么短--有一行,有分支用--在代码最后去调用Unlock就得更直截了。在更复杂的临界区的应用中,尤其是必须要尽早处理错误并返回的情下,就很去(靠人)判断对Lock和Unlock的用是在所有路中都能够严格配的了。Go言里的defer直就是这种情况下的救星:我用defer来调用Unlock临界区会隐式地延伸到函作用域的最后,这样我们就从“总要记得在函数返回之后或者发生错误返回时要记得调用一次Unlock”这种状态中获得了解放。Go会自动帮我们完成些事情。
```go
func Balance() int {
@@ -64,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!
@@ -82,9 +82,9 @@ func Withdraw(amount int) bool {
}
```
數終於給出了正確的結果,但是有一點討厭的副作用。當過多的取款操作同時執行時balance可能會瞬時被減到0以下。可能引起一個併發的取款被不合邏輯地拒。所以如果Bob嚐試買一輛sports carAlice可能就沒辦法爲她的早咖啡付款了。里的問題是取款不是一原子操作:它包含了三個步驟,每一步都需要去獲取併釋放互斥,但任何一次都不會鎖上整取款流程。
数终于给出了正确的结果,但是有一点讨厌的副作用。当过多的取款操作同时执行时balance可能会瞬时被减到0以下。可能引起一个并发的取款被不合逻辑地拒。所以如果Bob尝试买一辆sports carAlice可能就没办法为她的早咖啡付款了。里的问题是取款不是一原子操作:它包含了三个步骤,每一步都需要去获取并释放互斥,但任何一次都不会锁上整取款流程。
理想情下,取款應該隻在整操作中得一次互斥。下面這樣的嚐試是錯誤的:
理想情下,取款应该只在整操作中得一次互斥。下面这样的尝试是错误的:
```go
// NOTE: incorrect!
@@ -100,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 {
@@ -134,6 +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

@@ -1,8 +1,8 @@
## 9.3. sync.RWMutex讀寫鎖
## 9.3. sync.RWMutex读写锁
在100刀的存款消失不做記録多少還是會讓我們有一些恐慌Bob了一程序,每秒運行幾百次來檢査他的銀行餘額。他在家,在工作中,甚至在他的手機上來運行這個程序。銀行註意到些陡增的流量使得存款和取款有了延,因所有的餘額査詢請求是順序執行的,這樣會互斥地獲得鎖,併且會暫時阻止其它的goroutine行。
在100刀的存款消失不做记录多少还是会让我们有一些恐慌Bob了一程序,每秒运行几百次来检查他的银行余额。他在家,在工作中,甚至在他的手机上来运行这个程序。银行注意到些陡增的流量使得存款和取款有了延,因所有的余额查询请求是顺序执行的,这样会互斥地获得锁,并且会暂时阻止其它的goroutine行。
Balance函數隻需要讀取變量的狀態,所以我們同時讓多個Balance調用併發運行事上是安全的,要在行的時候沒有存款或者取款操作就行。在這種場景下我需要一特殊型的,其允許多個隻讀操作併行執行,但操作完全互斥。這種鎖叫作“多讀單寫”鎖(multiple readers, single writer lock)Go言提供的這樣的鎖是sync.RWMutex
Balance函数只需要读取变量的状态,所以我们同时让多个Balance调用并发运行事上是安全的,要在行的时候没有存款或者取款操作就行。在这种场景下我需要一特殊型的,其允许多个只读操作并行执行,但操作完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock)Go言提供的这样的锁是sync.RWMutex
```go
var mu sync.RWMutex
@@ -14,11 +14,11 @@ func Balance() int {
}
```
Balance函數現在調用了RLock和RUnlock方法來獲取和放一個讀取或者共享。Deposit函數沒有變化,會調用mu.Lock和mu.Unlock方法來獲取和放一個寫或互斥
Balance函数现在调用了RLock和RUnlock方法来获取和放一个读取或者共享。Deposit函数没有变化,会调用mu.Lock和mu.Unlock方法来获取和放一个写或互斥
這次脩改後Bob的餘額査詢請求就可以彼此行地執行併且會很快地完成了。在更多的時間范圍可用,且存款求也能夠及時地被響應了。
这次修改后Bob的余额查询请求就可以彼此行地执行并且会很快地完成了。在更多的时间范围可用,且存款求也能够及时地被响应了。
RLock能在臨界區共享變量沒有任何入操作可用。一般來説,我們不應該假設邏輯上的隻讀函數/方法也不去更新某一些量。比如一方法功能是訪問一個變量,但它也有可能會同時去給一個內部的計數器+1(譯註:可能是記録這個方法的訪問次數啥的),或者去更新存--使卽時的調用能更快。如果有疑惑的話,請使用互斥
RLock能在临界区共享变量没有任何入操作可用。一般来说,我们不应该假设逻辑上的只读函数/方法也不去更新某一些量。比如一方法功能是访问一个变量,但它也有可能会同时去给一个内部的计数器+1(译注:可能是记录这个方法的访问次数啥的),或者去更新存--使即时的调用能更快。如果有疑惑的话,请使用互斥
RWMutex隻有當獲得鎖的大部分goroutine都是操作,而鎖在競爭條件下,也就是goroutine們必須等待才能取到鎖的時RWMutex才是最能帶來好處的。RWMutex需要更複雜的內部記録,所以會讓它比一般的無競爭鎖的mutex慢一些。
RWMutex只有当获得锁的大部分goroutine都是操作,而锁在竞争条件下,也就是goroutine们必须等待才能取到锁的时RWMutex才是最能带来好处的。RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。

View File

@@ -1,10 +1,10 @@
## 9.4. 存同步
## 9.4. 存同步
你可能比較糾結爲什麽Balance方法需要用到互斥件,無論是基channel是基互斥量。竟和存款不一,它由一個簡單的操作成,所以不碰到其它goroutine在其行"中"行其它的邏輯的風險。這里使用mutex有方面考。第一Balance不在其它操作比如Withdraw“中間”執行。第二(更重要)的是"同步"不僅僅是一堆goroutine執行順序的問題;同樣也會涉及到存的問題
你可能比较纠结为什么Balance方法需要用到互斥件,无论是基channel是基互斥量。竟和存款不一,它由一个简单的操作成,所以不碰到其它goroutine在其行"中"行其它的逻辑的风险。这里使用mutex有方面考。第一Balance不在其它操作比如Withdraw“中间”执行。第二(更重要)的是"同步"不仅仅是一堆goroutine执行顺序的问题;同样也会涉及到存的问题
現代計算機中可能有一堆理器,每一個都會有其本地存(local cache)。了效率,對內存的入一般在每一個處理器中緩衝,併在必要一起flush到主存。這種情況下這些數據可能會以與當初goroutine寫入順序不同的序被提交到主存。像channel通信或者互斥量操作這樣的原語會使處理器其聚集的入flushcommit這樣goroutine在某個時間點上的執行結果才能被其它理器上行的goroutine得到。
现代计算机中可能有一堆理器,每一个都会有其本地存(local cache)。了效率,对内存的入一般在每一个处理器中缓冲,并在必要一起flush到主存。这种情况下这些数据可能会以与当初goroutine写入顺序不同的序被提交到主存。像channel通信或者互斥量操作这样的原语会使处理器其聚集的入flushcommit这样goroutine在某个时间点上的执行结果才能被其它理器上行的goroutine得到。
一下下面代片段的可能出:
一下下面代片段的可能出:
```go
var x, y int
@@ -18,7 +18,7 @@ go func() {
}()
```
爲兩個goroutine是併發執行,併且訪問共享變量時也沒有互斥,會有數據競爭,所以程序的運行結果沒法預測的話也請不要驚訝。我可能希望它能打印出下面這四種結果中的一,相當於幾種不同的交錯執行時的情
为两个goroutine是并发执行,并且访问共享变量时也没有互斥,会有数据竞争,所以程序的运行结果没法预测的话也请不要惊讶。我可能希望它能打印出下面这四种结果中的一,相当于几种不同的交错执行时的情
```
y:0 x:1
@@ -27,19 +27,19 @@ x:1 y:1
y:1 x:1
```
第四行可以被解釋爲執行順序A1,B1,A2,B2或者B1,A1,A2,B2的執行結果。
然而實際的運行時還是有些情況讓我們有點驚訝
第四行可以被解释为执行顺序A1,B1,A2,B2或者B1,A1,A2,B2的执行结果。
然而实际的运行时还是有些情况让我们有点惊讶
```
x:0 y:0
y:0 x:0
```
但是根所使用的編譯CPU或者其它很多影因子,這兩種情況也是有可能生的。那麽這兩種情況要怎麽解釋呢?
但是根所使用的编译CPU或者其它很多影因子,这两种情况也是有可能生的。那么这两种情况要怎么解释呢?
在一個獨立的goroutine中每一個語句的執行順序是可以被保的;也就是goroutine是順序連貫的。但是在不使用channel且不使用mutex這樣的顯式同步操作,我們就沒法保事件在不同的goroutine中看到的執行順序是一致的了。管goroutine A中一定需要察到x=1行成功之後才會去讀取y但它沒法確保自己察得到goroutine B中y的所以A可能打印出y的一個舊版的值。
在一个独立的goroutine中每一个语句的执行顺序是可以被保的;也就是goroutine是顺序连贯的。但是在不使用channel且不使用mutex这样的显式同步操作,我们就没法保事件在不同的goroutine中看到的执行顺序是一致的了。管goroutine A中一定需要察到x=1行成功之后才会去读取y但它没法确保自己察得到goroutine B中y的所以A可能打印出y的一个旧版的值。
管去理解併發的一種嚐試是去將其運行理解不同goroutine句的交錯執行,但看看上面的例子,這已經不是代的編譯器和cpu的工作方式了。因爲賦值和打印指向不同的量,編譯器可能會斷定兩條語句的序不會影響執行結果,併且會交換兩個語句的執行順序。如果兩個goroutine在不同的CPU上行,每一核心有自己的存,這樣一個goroutine的寫入對於其它goroutine的Print在主存同步之前就是不可的了。
管去理解并发的一种尝试是去将其运行理解不同goroutine句的交错执行,但看看上面的例子,这已经不是代的编译器和cpu的工作方式了。因为赋值和打印指向不同的量,编译器可能会断定两条语句的序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上行,每一核心有自己的存,这样一个goroutine的写入对于其它goroutine的Print在主存同步之前就是不可的了。
所有併發的問題都可以用一致的、簡單的旣定的模式來規避。所以可能的話,將變量限定在goroutine部;如果是多goroutine都需要訪問的變量,使用互斥條件來訪問
所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine部;如果是多goroutine都需要访问的变量,使用互斥条件来访问

View File

@@ -1,12 +1,12 @@
## 9.5. sync.Once初始化
如果初始化成本比大的,那麽將初始化延到需要的候再去做就是一個比較好的選擇。如果在程序啟動的時候就去做這類的初始化的話會增加程序的啟動時間併且因爲執行的候可能也不需要這些變量所以實際上有一些浪費。讓我們在本章早一些候看到的icons量:
如果初始化成本比大的,那么将初始化延到需要的候再去做就是一个比较好的选择。如果在程序启动的时候就去做这类的初始化的话会增加程序的启动时间并且因为执行的候可能也不需要这些变量所以实际上有一些浪费。让我们在本章早一些候看到的icons量:
```go
var icons map[string]image.Image
```
這個版本的Icon用到了初始化(lazy initialization)。
这个版本的Icon用到了初始化(lazy initialization)。
```go
func loadIcons() {
@@ -27,9 +27,9 @@ func Icon(name string) image.Image {
}
```
如果一個變量隻被一個單獨的goroutine所訪問的話,我可以使用上面的這種模闆但這種模闆在Icon被併發調用時併不安全。就像前面行的那Deposit(存款)函數一樣Icon函也是由多個步驟組成的:首先測試icons是否空,然load些icons後將icons更新爲一個非空的值。直覺會告訴我們最差的情是loadIcons函被多次訪問會帶來數據競爭。當第一goroutine在忙着loading些icons的候,另一goroutine入了Icon函數,發現變量是nil後也會調用loadIcons函
如果一个变量只被一个单独的goroutine所访问的话,我可以使用上面的这种模板但这种模板在Icon被并发调用时并不安全。就像前面行的那Deposit(存款)函数一样Icon函也是由多个步骤组成的:首先测试icons是否空,然load些icons后将icons更新为一个非空的值。直觉会告诉我们最差的情是loadIcons函被多次访问会带来数据竞争。当第一goroutine在忙着loading些icons的候,另一goroutine入了Icon函数,发现变量是nil后也会调用loadIcons函
過這種直覺是錯誤的。(我希望在你從現在開始能夠構建自己對併發的直,也就是説對併發的直覺總是不能被信任的!)迴憶一下9.4。因缺少式的同步,編譯器和CPU是可以意地去更改訪問內存的指令序,以任意方式,要保每一goroutine自己的執行順序一致。其中一可能loadIcons的句重排是下面這樣。它在填icons量的值之前先用一空map初始化icons量。
过这种直觉是错误的。(我希望在你从现在开始能够构建自己对并发的直,也就是说对并发的直觉总是不能被信任的!)回忆一下9.4。因缺少式的同步,编译器和CPU是可以意地去更改访问内存的指令序,以任意方式,要保每一goroutine自己的执行顺序一致。其中一可能loadIcons的句重排是下面这样。它在填icons量的值之前先用一空map初始化icons量。
```go
func loadIcons() {
@@ -41,9 +41,9 @@ func loadIcons() {
}
```
因此,一goroutine在檢査icons是非空,也不能就假設這個變量的初始化流程已走完了(譯註:可能是塞了空map里面的值還沒填完,也就是填值的句都沒執行完呢)。
因此,一goroutine在检查icons是非空,也不能就假设这个变量的初始化流程已走完了(译注:可能是塞了空map里面的值还没填完,也就是填值的句都没执行完呢)。
簡單且正的保所有goroutine能夠觀察到loadIcons效果的方式是用一mutex同步檢査
简单且正的保所有goroutine能够观察到loadIcons效果的方式是用一mutex同步检查
```go
var mu sync.Mutex // guards icons
@@ -60,7 +60,7 @@ func Icon(name string) image.Image {
}
```
然而使用互斥訪問icons的代就是沒有辦法對該變量進行併發訪問,卽使變量已被初始化完且再也不會進行變動。這里我可以引入一個允許多讀的鎖
然而使用互斥访问icons的代就是没有办法对该变量进行并发访问,即使变量已被初始化完且再也不会进行变动。这里我可以引入一个允许多读的锁
```go
var mu sync.RWMutex // guards icons
@@ -87,9 +87,9 @@ 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函吧:
上面的模使我的程序能更好的并发,但是有一点太复杂且容易出。幸的是sync包为我们提供了一个专门的方案来解决这种一次性初始化的问题sync.Once。概念上来讲,一次性的初始化需要一互斥量mutex和一boolean变量来记录初始化是不是已完成了;互斥量用来保护boolean量和客户端数据结构。Do这个唯一的方法需要接收初始化函数作为其参数。让我们用sync.Once来简化前面的Icon函吧:
```go
var loadIconsOnce sync.Once
@@ -101,6 +101,6 @@ func Icon(name string) image.Image {
}
```
每一次Do(loadIcons)的調用都會鎖定mutex併會檢査boolean量。在第一次調用時,變量的值是falseDo會調用loadIcons併會將boolean設置爲true。隨後的調用什都不但是mutex同步會保證loadIcons對內存(里其就是指icons量啦)生的效果能夠對所有goroutine可。用這種方式使用sync.Once的,我們能夠避免在量被建完成之前和其它goroutine共享該變量。
每一次Do(loadIcons)的用都会锁定mutex并会检查boolean量。在第一次调用时,变量的值是falseDo会调用loadIcons并会将boolean设置为true。随后的调用什都不但是mutex同步会保证loadIcons对内存(里其就是指icons量啦)生的效果能够对所有goroutine可。用这种方式使用sync.Once的,我们能够避免在量被建完成之前和其它goroutine共享该变量。
**練習 9.2**2.6.2中的PopCount的例子使用sync.Once在第一次需要用到的時候進行初始化。(雖然實際上,PopCount這樣很小且高度化的函數進行同步可能代價沒法接受)
**练习 9.2**2.6.2中的PopCount的例子使用sync.Once在第一次需要用到的时候进行初始化。(虽然实际上,PopCount这样很小且高度化的函数进行同步可能代价没法接受)

View File

@@ -1,11 +1,11 @@
## 9.6. 競爭條件檢測
## 9.6. 竞争条件检测
使我小心到不能再小心,但在併發程序中犯錯還是太容易了。幸的是Go的runtime和工具鏈爲我們裝備了一個複雜但好用的動態分析工具,競爭檢査器(the race detector)。
使我小心到不能再小心,但在并发程序中犯错还是太容易了。幸的是Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。
要在go buildgo run或者go test命令面加上-race的flag會使編譯器創建一你的用的“改”版或者一個附帶了能夠記録所有行期共享變量訪問工具的test併且會記録下每一個讀或者共享量的goroutine的身份信息。另外改版的程序會記録下所有的同步事件比如gochannel操作以及(\*sync.Mutex).Lock(\*sync.WaitGroup).Wait等等的調用。(完整的同步事件集合是在The Go Memory Model文中有明,該文檔是和言文放在一起的。譯註https://golang.org/ref/mem)
要在go buildgo run或者go test命令面加上-race的flag会使编译器创建一你的用的“改”版或者一个附带了能够记录所有行期共享变量访问工具的test并且会记录下每一个读或者共享量的goroutine的身份信息。另外改版的程序会记录下所有的同步事件比如gochannel操作以及(\*sync.Mutex).Lock(\*sync.WaitGroup).Wait等等的用。(完整的同步事件集合是在The Go Memory Model文中有明,该文档是和言文放在一起的。译注https://golang.org/ref/mem)
競爭檢査器會檢査這些事件,會尋找在哪一goroutine中出現了這樣的case例如其或者了一共享量,這個共享量是被另一goroutine在沒有進行榦預同步操作便直接入的。這種情況也就表明了是對一個共享量的併發訪問,卽數據競爭。這個工具打印一份告,容包含量身份,取和入的goroutine中活的函數的調用棧。這些信息在定位問題時通常很有用。9.7節中會有一個競爭檢査器的實戰樣例。
竞争检查器会检查这些事件,会寻找在哪一goroutine中出现了这样的case例如其或者了一共享量,这个共享量是被另一goroutine在没有进行干预同步操作便直接入的。这种情况也就表明了是对一个共享量的并发访问,即数据竞争。这个工具打印一份告,容包含量身份,取和入的goroutine中活的函数的调用栈。这些信息在定位问题时通常很有用。9.7节中会有一个竞争检查器的实战样例。
競爭檢査器會報告所有的已經發生的數據競爭。然而,它隻能檢測到運行時的競爭條件;不能明之後不會發生數據競爭。所以了使結果盡量正確,請保證你的測試併發地覆到了你到包。
竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;不能明之后不会发生数据竞争。所以了使结果尽量正确,请保证你的测试并发地覆到了你到包。
需要外的記録,因此構建時加了競爭檢測的程序跑起來會慢一些,且需要更大的存,卽時是這樣,這些代價對於很多生産環境的工作來説還是可以接受的。對於一些偶發的競爭條件來説,讓競爭檢査器來榦活可以節省無數日夜的debugging。(譯註:多少服端C和C艹程序員爲此盡摺腰)
需要外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的存,即时是这样,这些代价对于很多生产环境的工作来说还是可以接受的。对于一些偶发的竞争条件来说,让竞争检查器来干活可以节省无数日夜的debugging。(译注:多少服端C和C艹程序员为此尽折腰)

View File

@@ -1,8 +1,8 @@
## 9.7. 示例: 併發的非阻塞
## 9.7. 示例: 并发的非阻塞
中我們會做一個無阻塞的存,這種工具可以助我們來解決現實世界中併發程序出現但沒有現成的可以解決的問題。這個問題叫作存(memoizing)函數(譯註Memoization的定 memoization 一是Donald Michie 根拉丁memorandum杜撰的一個詞。相應的動詞、過去分、ing形式有memoiz、memoized、memoizing.),也就是,我需要存函的返迴結果,這樣在對函數進行調用的候,我們就隻需要一次算,之後隻要返迴計算的果就可以了。我的解方案會是併發安全且避免對整個緩存加鎖而導致所有操作都去爭一個鎖的設計
中我们会做一个无阻塞的存,这种工具可以助我们来解决现实世界中并发程序出现但没有现成的可以解决的问题。这个问题叫作存(memoizing)函数(译注Memoization的定 memoization 一是Donald Michie 根拉丁memorandum杜撰的一个词。相应的动词、过去分、ing形式有memoiz、memoized、memoizing.),也就是,我需要存函的返回结果,这样在对函数进行调用的候,我们就只需要一次算,之后只要返回计算的果就可以了。我的解方案会是并发安全且避免对整个缓存加锁而导致所有操作都去争一个锁的设计
們將使用下面的httpGetBody函數作爲我們需要存的函的一個樣例。這個函數會去進行HTTP GET請求併且獲取http響應body。對這個函數的調用本身開銷是比大的,所以我們盡量盡量避免在不必要的候反複調用。
们将使用下面的httpGetBody函数作为我们需要存的函的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比大的,所以我们尽量尽量避免在不必要的候反复调用。
```go
func httpGetBody(url string) (interface{}, error) {
@@ -15,9 +15,9 @@ func httpGetBody(url string) (interface{}, error) {
}
```
一行稍微藏了一些細節。ReadAll會返迴兩個結果,一[]byte數組和一個錯誤,不過這兩個對象可以被賦值給httpGetBody的返迴聲明里的interface{}和error型,所以我也就可以這樣返迴結果併且不需要外的工作了。我在httpGetBody中選用這種返迴類型是了使其可以與緩存匹配。
一行稍微藏了一些细节。ReadAll会返回两个结果,一[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error型,所以我也就可以这样返回结果并且不需要外的工作了。我在httpGetBody中选用这种返回类型是了使其可以与缓存匹配。
下面是我們要設計的cache的第一“草稿”:
下面是我们要设计的cache的第一“草稿”:
<u><i>gopl.io/ch9/memo1</i></u>
```go
@@ -54,9 +54,9 @@ func (memo *Memo) Get(key string) (interface{}, error) {
}
```
Memo實例會記録需要存的函f(類型爲Func),以及緩存內容(里面是一string到result映射的map)。每一result都是都是簡單的函數返迴的值對兒--一值和一個錯誤值。繼續下去我們會展示一些Memo的變種,不所有的例子都遵循些上面的些方面。
Memo实例会记录需要存的函f(类型为Func),以及缓存内容(里面是一string到result映射的map)。每一result都是都是简单的函数返回的值对儿--一值和一个错误值。继续下去我们会展示一些Memo的变种,不所有的例子都遵循些上面的些方面。
下面是一使用Memo的例子。對於流入的URL的每一元素我們都會調用Get打印調用延以及其返迴的數據大小的log
下面是一使用Memo的例子。对于流入的URL的每一元素我们都会调用Get打印用延以及其返回的数据大小的log
```go
m := memo.New(httpGetBody)
@@ -71,7 +71,7 @@ for url := range incomingURLs() {
}
```
可以使用測試包(第11章的主題)來繫統地鑒定緩存的效果。下面的測試輸出,我可以看到URL流包含了一些重的情況,盡管我第一次每一URL的(\*Memo).Get的調用都花上百毫秒,但第二次就需要花1毫秒就可以返完整的數據了。
可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。下面的测试输出,我可以看到URL流包含了一些重的情况,尽管我第一次每一URL的(\*Memo).Get的用都花上百毫秒,但第二次就需要花1毫秒就可以返完整的数据了。
```
$ go test -v gopl.io/ch9/memo1
@@ -89,9 +89,9 @@ PASS
ok gopl.io/ch9/memo1 1.257s
```
這個測試是順序地去做所有的調用的。
这个测试是顺序地去做所有的用的。
於這種彼此立的HTTP求可以很好地併發,我可以把這個測試改成併發形式。可以使用sync.WaitGroup等待所有的求都完成之再返
于这种彼此立的HTTP求可以很好地并发,我可以把这个测试改成并发形式。可以使用sync.WaitGroup等待所有的求都完成之再返
```go
m := memo.New(httpGetBody)
@@ -113,9 +113,9 @@ n.Wait()
```
這次測試跑起更快了,然而不幸的是貌似這個測試不是每次都能正常工作。我們註意到有一些意料之外的cache miss(存未命中),或者命中了存但卻返迴了錯誤的值,或者甚至直接崩
这次测试跑起更快了,然而不幸的是貌似这个测试不是每次都能正常工作。我们注意到有一些意料之外的cache miss(存未命中),或者命中了存但却返回了错误的值,或者甚至直接崩
但更糟糕的是,有時候這個程序是能正確的運行(:也就是最人崩的偶bug),所以我甚至可能都不會意識到這個程序有bug。。但是我可以使用-race這個flag來運行程序,競爭檢測器(§9.6)打印像下面這樣的報告:
但更糟糕的是,有时候这个程序是能正确的运行(:也就是最人崩的偶bug),所以我甚至可能都不会意识到这个程序有bug。。但是我可以使用-race这个flag来运行程序,竞争检测器(§9.6)打印像下面这样的报告:
```
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
@@ -138,7 +138,7 @@ Found 1 data race(s)
FAIL gopl.io/ch9/memo1 2.393s
```
memo.go的32行出現了兩次,明有兩個goroutine在有同步榦預的情下更新了cache map。表明Get不是併發安全的,存在數據競爭
memo.go的32行出现了两次,明有两个goroutine在有同步干预的情下更新了cache map。表明Get不是并发安全的,存在数据竞争
```go
28 func (memo *Memo) Get(key string) (interface{}, error) {
@@ -151,7 +151,7 @@ memo.go的32行出現了兩次説明有兩個goroutine在沒有同步榦預
35 }
```
簡單的使cache併發安全的方式是使用基於監控的同步。隻要給Memo加上一mutex在Get的一開始獲取互斥return的時候釋放鎖,就可以cache的操作生在臨界區內了:
简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一mutex在Get的一开始获取互斥return的时候释放锁,就可以cache的操作生在临界区内了:
<u><i>gopl.io/ch9/memo2</i></u>
```go
@@ -177,9 +177,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
測試依然併發進行,但這迴競爭檢査器“沉默”了。不幸的是對於Memo的這一點改變使我完全失了併發的性能優點。每次f的調用期間都會持有Get將本來可以併行運行的I/O操作串行化了。我本章的目的是完成一個無鎖緩存,而不是現在這樣的將所有求串行化的函數的緩存。
测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我完全失了并发的性能优点。每次f的用期间都会持有Get将本来可以并行运行的I/O操作串行化了。我本章的目的是完成一个无锁缓存,而不是现在这样的将所有求串行化的函数的缓存。
下一Get的實現,調用Get的goroutine會兩次獲取鎖:査找階段獲取一次,如果査找沒有返任何容,那麽進入更新階段會再次取。在這兩次獲取鎖的中間階其它goroutine可以意使用cache。
下一Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返任何容,那么进入更新阶段会再次取。在这两次获取锁的中间阶其它goroutine可以意使用cache。
<u><i>gopl.io/ch9/memo3</i></u>
```go
@@ -200,9 +200,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
這些脩改使性能再次得到了提但有一些URL被取了次。這種情況在兩個以上的goroutine同一時刻調用Get來請求同的URL時會發生。多goroutine一起査詢cache發現沒有值,然一起調用f這個慢不拉的函。在得到結果後,也都去去更新map。其中一個獲得的結果會覆蓋掉另一個的結果。
这些修改使性能再次得到了提但有一些URL被取了次。这种情况在两个以上的goroutine同一时刻调用Get来请求同的URL时会发生。多goroutine一起查询cache发现没有值,然一起用f这个慢不拉的函。在得到结果后,也都去去更新map。其中一个获得的结果会覆盖掉另一个的结果。
理想情下是應該避免掉多的工作的。而這種“避免”工作一般被稱爲duplicate suppression(重複抑製/避免)。下面版本的Memo每一map元素都是指向一個條目的指。每一個條目包含對函數f調用結果的內容緩存。之前不同的是次entry包含了一叫ready的channel。在目的果被置之後,這個channel就會被關閉以向其它goroutine播(§8.9)去讀取該條目內的結果是安全的了。
理想情下是应该避免掉多的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一map元素都是指向一个条目的指。每一个条目包含对函数f调用结果的内容缓存。之前不同的是次entry包含了一叫ready的channel。在目的果被置之后,这个channel就会被关闭以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。
<u><i>gopl.io/ch9/memo4</i></u>
```go
@@ -245,17 +245,17 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
在Get函包括下面些步了:取互斥鎖來保護共享量cache map査詢map中是否存在指定目,如果有找到那分配空插入一個新條目,放互斥。如果存在目的且其值沒有寫入完成(也就是有其它的goroutine在調用f這個慢函數)時goroutine必等待值ready之才能讀到條目的果。而想知道是否ready的,可以直接ready channel中取,由於這個讀取操作在channel關閉之前一直是阻塞。
在Get函包括下面些步了:取互斥锁来保护共享量cache map查询map中是否存在指定目,如果有找到那分配空插入一个新条目,放互斥。如果存在目的且其值没有写入完成(也就是有其它的goroutine在用f这个慢函数)时goroutine必等待值ready之才能读到条目的果。而想知道是否ready的,可以直接ready channel中取,由于这个读取操作在channel关闭之前一直是阻塞。
如果沒有條目的需要向map中插入一個沒有ready的目,前正在調用的goroutine就需要負責調用慢函、更新目以及向其它所有goroutine廣播條目已ready可的消息了。
如果没有条目的需要向map中插入一个没有ready的目,前正在用的goroutine就需要负责调用慢函、更新目以及向其它所有goroutine广播条目已ready可的消息了。
目中的e.res.value和e.res.err量是在多goroutine之共享的。創建條目的goroutine同時也會設置條目的值其它goroutine在收到"ready"的播消息之立刻會去讀取條目的值。盡管會被多goroutine同時訪問,但卻併不需要互斥。ready channel的關閉一定會發生在其它goroutine接收到播事件之前,因此第一goroutine對這些變量的操作是一定生在這些讀操作之前的。不會發生數據競爭
目中的e.res.value和e.res.err量是在多goroutine之共享的。创建条目的goroutine同时也会设置条目的值其它goroutine在收到"ready"的广播消息之立刻会去读取条目的值。尽管会被多goroutine同时访问,但却并不需要互斥。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一goroutine对这些变量的操作是一定生在这些读操作之前的。不会发生数据竞争
這樣併發、不重複、無阻塞的cache就完成了。
这样并发、不重复、无阻塞的cache就完成了。
上面這樣Memo的實現使用了一互斥量來保護多個goroutine調用Get的共享map量。不妨把這種設計和前面提到的把map量限在一個單獨的monitor goroutine的方案做一些比,者在調用Get需要消息。
上面这样Memo的实现使用了一互斥量来保护多个goroutine用Get的共享map量。不妨把这种设计和前面提到的把map量限在一个单独的monitor goroutine的方案做一些比,者在用Get需要消息。
Func、result和entry的明和之前保持一致:
Func、result和entry的明和之前保持一致:
```go
// Func is the type of the function to memoize.
@@ -273,7 +273,7 @@ type entry struct {
}
```
然而Memo類型現在包含了一叫做requests的channelGet的調用方用這個channel和monitor goroutine通信。requests channel中的元素型是request。Get的調用方會把這個結構中的兩組key都填充好實際上用這兩個變量來對函數進行緩存的。另一叫response的channel被拿來發送響應結果。這個channel隻會傳迴一個單獨的值。
然而Memo类型现在包含了一叫做requests的channelGet的用方用这个channel和monitor goroutine通信。requests channel中的元素型是request。Get的用方会把这个结构中的两组key都填充好实际上用这两个变量来对函数进行缓存的。另一叫response的channel被拿来发送响应结果。这个channel只会传回一个单独的值。
<u><i>gopl.io/ch9/memo5</i></u>
```go
@@ -301,9 +301,9 @@ func (memo *Memo) Get(key string) (interface{}, error) {
func (memo *Memo) Close() { close(memo.requests) }
```
上面的Get方法會創建一response channel把它放request結構中,然後發送給monitor goroutine後馬上又接收到它。
上面的Get方法会创建一response channel把它放request结构中,然后发送给monitor goroutine后马上又接收到它。
cache量被限在了monitor goroutine (\*Memo).server中下面看到。monitor在循中一直讀取請直到request channel被Close方法關閉。每一個請求都會去査詢cache如果有找到目的,那麽就會創建/插入一新的目。
cache量被限在了monitor goroutine (\*Memo).server中下面看到。monitor在循中一直读取请直到request channel被Close方法关闭。每一个请求都会去查询cache如果有找到目的,那么就会创建/插入一新的目。
```go
func (memo *Memo) server(f Func) {
@@ -335,13 +335,13 @@ func (e *entry) deliver(response chan<- result) {
}
```
和基互斥量的版本似,第一個對某個key的求需要負責去調用函數f併傳入這個key將結果存在目里,併關閉ready channel來廣播條目的ready消息。使用(\*entry).call完成上述工作。
和基互斥量的版本似,第一个对某个key的求需要负责去调用函数f并传入这个key将结果存在目里,并关闭ready channel来广播条目的ready消息。使用(\*entry).call完成上述工作。
接着同一key的請求會發現map中已有了存在的目,然後會等待結果變爲ready併將結果從response發送給客戶端的goroutien。上述工作是用(\*entry).deliver完成的。call和deliver方法的調用必在自己的goroutine中行以保monitor goroutines不因此而被阻塞住而沒法處理新的求。
接着同一key的请求会发现map中已有了存在的目,然后会等待结果变为ready并将结果从response发送给客户端的goroutien。上述工作是用(\*entry).deliver完成的。call和deliver方法的用必在自己的goroutine中行以保monitor goroutines不因此而被阻塞住而没法处理新的求。
這個例子明我們無論可以用上鎖,還是通信建立併發程序都是可行的。
这个例子明我们无论可以用上锁,还是通信建立并发程序都是可行的。
上面的兩種方案不好特定情境下哪更好,不了解他們還是有值的。有時候從一種方式切到另一可以使你的代碼更爲簡潔。(譯註:不是好的golang推崇通信併發麽)
上面的两种方案不好特定情境下哪更好,不了解他们还是有值的。有时候从一种方式切到另一可以使你的代码更为简洁。(译注:不是好的golang推崇通信并发么)
**練習 9.3** 展Func型和(\*Memo).Get方法支持調用方提供一個可選的done channel使其具備通過該channel取消整操作的能力(§8.9)。一被取消了的Func的調用結果不應該被緩存。
**练习 9.3** 展Func型和(\*Memo).Get方法支持用方提供一个可选的done channel使其具备通过该channel取消整操作的能力(§8.9)。一被取消了的Func的调用结果不应该被缓存。

View File

@@ -1,7 +1,7 @@
### 9.8.1. 動態棧
### 9.8.1. 动态栈
每一OS程都有一固定大小的內存塊(一般是2MB)來做棧,這個棧會用來存儲當前正在被調用或起(指在調用其它函數時)的函數的內部變量。這個固定大小的棧同時很大又很小。因2MB的棧對於一個小小的goroutine來説是很大的存浪,比如對於我們用到的,一個隻是用WaitGroup之後關閉channel的goroutine來説。而對於go程序來説同時創建成百上韆個gorutine是非常普遍的如果每一goroutine都需要這麽大的棧的話,那這麽多的goroutine就不太可能了。除去大小的問題之外,固定大小的棧對於更複雜或者更深次的遞歸函數調用來説顯然是不的。改固定的大小可以提陞空間的利用率允許創建更多的程,且可以允更深的遞歸調用,不過這兩者是法同時兼備的。
每一OS线程都有一固定大小的内存块(一般是2MB)来做栈,这个栈会用来存储当前正在被用或起(指在用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。因2MB的栈对于一个小小的goroutine来说是很大的存浪,比如对于我们用到的,一个只是用WaitGroup之后关闭channel的goroutine来说。而对于go程序来说同时创建成百上千个gorutine是非常普遍的如果每一goroutine都需要这么大的栈的话,那这么多的goroutine就不太可能了。除去大小的问题之外,固定大小的栈对于更复杂或者更深次的递归函数调用来说显然是不的。改固定的大小可以提升空间的利用率允许创建更多的线程,且可以允更深的递归调用,不过这两者是法同时兼备的。
相反,一goroutine以一很小的棧開始其生命期,一般需要2KB。一goroutine的,和操作繫統線程一樣,會保存其活躍或掛起的函數調用的本地但是和OS程不太一的是一goroutine的大小不是固定的;的大小會根據需要動態地伸。而goroutine的的最大值有1GB傳統的固定大小的線程棧要大得多,管一般情大多goroutine都不需要這麽大的
相反,一goroutine以一很小的栈开始其生命期,一般需要2KB。一goroutine的,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地但是和OS线程不太一的是一goroutine的大小不是固定的;的大小会根据需要动态地伸。而goroutine的的最大值有1GB传统的固定大小的线程栈要大得多,管一般情大多goroutine都不需要这么大的
練習 9.4: 建一流水程序支持用channel接任意量的goroutine在跑爆存之前,可以建多少流水線階段?一個變量通過整個流水需要用多久?(這個練習題翻譯不是很定。。)
练习 9.4: 建一流水线程序支持用channel接任意量的goroutine在跑爆存之前,可以建多少流水线阶段?一个变量通过整个流水线需要用多久?(这个练习题翻译不是很定。。)

View File

@@ -1,9 +1,9 @@
### 9.8.2. Goroutine調
### 9.8.2. Goroutine
OS線程會被操作繫統內核調度。每毫秒,一硬件計時器會中斷處理器,這會調用一叫作scheduler的核函數。這個函數會掛起當前執行的線程併保存存中它的寄存器容,檢査線程列表併決定下一次哪個線程可以被行,併從內存中恢複該線程的寄存器信息,然後恢複執行該線程的現場併開始執行線程。因操作繫統線程是被核所調度,所以從一個線程向另一“移”需要完整的上下文切,也就是,保存一個用戶線程的狀態到內存,恢另一個線程的到寄存器,然更新調度器的數據結構。這幾步操作很慢,因其局部性很差需要幾次內存訪問,併且會增加行的cpu期。
OS线程会被操作系统内核调度。每毫秒,一硬件计时器会中断处理器,这会调用一叫作scheduler的核函数。这个函数会挂起当前执行的线程并保存存中它的寄存器容,检查线程列表并决定下一次哪个线程可以被行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。因操作系统线程是被核所度,所以从一个线程向另一“移”需要完整的上下文切,也就是,保存一个用户线程的状态到内存,恢另一个线程的到寄存器,然更新度器的数据结构。这几步操作很慢,因其局部性很差需要几次内存访问,并且会增加行的cpu期。
Go的運行時包含了其自己的調度器,這個調度器使用了一些技手段比如m:n調度,因爲其會在n操作繫統線程上多工(調度)mgoroutine。Go調度器的工作和核的調度是相似的,但是這個調度器隻關註單獨的Go程序中的goroutine(譯註:按程序立)。
Go的运行时包含了其自己的度器,这个调度器使用了一些技手段比如m:n度,因为其会在n操作系统线程上多工(度)mgoroutine。Go度器的工作和核的度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(译注:按程序立)。
和操作繫統的線程調度不同的是Go調度器不是用一硬件定器而是被Go言"建"本身進行調度的。例如當一個goroutine調用了time.Sleep或者被channel調用或者mutex操作阻塞時,調度器使其入休眠併開始執行另一goroutine直到時機到了再去醒第一goroutine。因爲因爲這種調度方式不需要進入內核的上下文,所以重新調度一goroutine比調度一個線程代要低得多。
和操作系统的线程调度不同的是Go度器不是用一硬件定器而是被Go言"建"本身进行调度的。例如当一个goroutine用了time.Sleep或者被channel用或者mutex操作阻塞时,调度器使其入休眠并开始执行另一goroutine直到时机到了再去醒第一goroutine。因为因为这种调度方式不需要进入内核的上下文,所以重新度一goroutine比度一个线程代要低得多。
練習 9.5: 寫一個有兩個goroutine的程序兩個goroutine會向兩個無buffer channel反複地發送ping-pong消息。這樣的程序每秒可以支持多少次通信?
练习 9.5: 写一个有两个goroutine的程序两个goroutine会向两个无buffer channel反复地发送ping-pong消息。这样的程序每秒可以支持多少次通信?

View File

@@ -1,8 +1,8 @@
### 9.8.3. GOMAXPROCS
Go的調度器使用了一叫做GOMAXPROCS的變量來決定會有多少操作繫統的線程同時執行Go的代。其默的值是運行機器上的CPU的核心,所以在一有8核心的器上時,調度器一次在8OS程上去調度GO代。(GOMAXPROCS是前面的m:n調度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一個對應的線程來做調度的。在I/O中或繫統調用中或調用非Go言函數時,是需要一個對應的操作繫統線程的但是GOMAXPROCS不需要將這幾種情況計數在內
Go的度器使用了一叫做GOMAXPROCS的变量来决定会有多少操作系统的线程同时执行Go的代。其默的值是运行机器上的CPU的核心,所以在一有8核心的器上时,调度器一次在8OS线程上去度GO代。(GOMAXPROCS是前面的m:n度中的n)。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或用非Go言函数时,是需要一个对应的操作系统线程的但是GOMAXPROCS不需要将这几种情况计数在内
你可以用GOMAXPROCS的環境變量呂顯式地控製這個參數,或者也可以在運行時用runtime.GOMAXPROCS函數來脩改它。我在下面的小程序中看到GOMAXPROCS的效果這個程序會無限打印0和1。
你可以用GOMAXPROCS的环境变量吕显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。我在下面的小程序中看到GOMAXPROCS的效果这个程序会无限打印0和1。
```go
@@ -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有多少核心?
练习9.6: 测试一下算密集型的并发程序(练习8.5那的)被GOMAXPROCS怎样影响到。在你的电脑上最佳的值是多少?你的电脑CPU有多少核心?

View File

@@ -1,10 +1,10 @@
### 9.8.4. Goroutine有ID
### 9.8.4. Goroutine有ID
在大多支持多程的操作繫統和程序言中,前的程都有一個獨特的身份(id)併且這個身份信息可以以一普通值的形式被被很容易地取到,典型的可以是一integer或者指值。這種情況下我做一抽象化的thread-local storage(程本地存,多線程編程中不希望其它線程訪問的內容)就很容易,需要以程的id作key的一map就可以解決問題,每一個線程以其id就能從中獲取到值,且和其它程互不突。
在大多支持多线程的操作系统和程序言中,前的线程都有一个独特的身份(id)并且这个身份信息可以以一普通值的形式被被很容易地取到,典型的可以是一integer或者指值。这种情况下我做一抽象化的thread-local storage(线程本地存,多线程编程中不希望其它线程访问的内容)就很容易,需要以线程的id作key的一map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不突。
goroutine有可以被程序員獲取到的身份(id)的概念。這一點是設計上故意而之,由thread-local storage總是會被濫用。比如,一web server是用一支持tls的語言實現的,而非常普遍的是很多函數會去尋找HTTP求的信息,代表它就是去其存儲層(這個存儲層有可能是tls)找的。就像是那些分依全局量的程序一樣,會導致一非健康的“距外行”,在這種行爲下,一個函數的行可能不是由其自己部的量所定,而是由其所行在的程所定。因此,如果程本身的身份會改變——比如一些worker程之的——那麽函數的行爲就會變得神祕莫測
goroutine有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而之,由thread-local storage总是会被滥用。比如,一web server是用一支持tls的语言实现的,而非常普遍的是很多函数会去寻找HTTP求的信息,代表它就是去其存储层(这个存储层有可能是tls)找的。就像是那些分依全局量的程序一样,会导致一非健康的“距外行”,在这种行为下,一个函数的行可能不是由其自己部的量所定,而是由其所行在的线程所定。因此,如果线程本身的身份会改变——比如一些worker线程之的——那么函数的行为就会变得神秘莫测
Go鼓勵更爲簡單的模式,這種模式下參數對函數的影都是式的。這樣不僅使程序得更易,而且會讓我們自由地向一些定的函分配子任務時不用心其身份信息影響行爲
Go鼓励更为简单的模式,这种模式下参数对函数的影都是式的。这样不仅使程序得更易,而且会让我们自由地向一些定的函分配子任务时不用心其身份信息影响行为
現在應該已經明白了寫一個Go程序所需要的所有言特性信息。在後面兩章節中,我們會迴顧一些之前的例和工具,支持我們寫出更大模的程序:如何將一個工程組織成一列的包,如果取,建,測試,性能測試,剖析,寫文檔,併且將這些包分享出去。
现在应该已经明白了写一个Go程序所需要的所有言特性信息。在后面两章节中,我们会回顾一些之前的例和工具,支持我们写出更大模的程序:如何将一个工程组织成一列的包,如果取,建,测试,性能测试,剖析,写文档,并且将这些包分享出去。

View File

@@ -1,6 +1,6 @@
## 9.8. Goroutines和
## 9.8. Goroutines和线
在上一章中我們説goroutine和操作繫統的線程區别可以先忽略。盡管兩者的區别實際上隻是一量的别,但量變會引起質變的道理同樣適用於goroutine和程。在正是我們來區分開兩者的最佳時機
在上一章中我们说goroutine和操作系统的线程区别可以先忽略。尽管两者的区别实际上只是一量的别,但量变会引起质变的道理同样适用于goroutine和线程。在正是我们来区分开两者的最佳时机
{% include "./ch9-08-1.md" %}

View File

@@ -1,5 +1,5 @@
# 第九章 基共享量的併發
# 第九章 基共享量的并发
前一章我們介紹了一些使用goroutine和channel這樣直接而自然的方式來實現併發的方法。然而這樣做我們實際上屏蔽掉了在寫併發代碼時必須處理的一些重要而且微的問題
前一章我们介绍了一些使用goroutine和channel这样直接而自然的方式来实现并发的方法。然而这样做我们实际上屏蔽掉了在写并发代码时必须处理的一些重要而且微的问题
在本章中,我們會細致地了解併發機製。尤其是在多goroutine之的共享量,併發問題的分析手段,以及解決這些問題的基本模式。最後我們會解釋goroutine和操作繫統線程之的技上的一些别。
在本章中,我们会细致地了解并发机制。尤其是在多goroutine之的共享量,并发问题的分析手段,以及解决这些问题的基本模式。最后我们会解释goroutine和操作系统线程之的技上的一些别。