Fixes #198
This commit is contained in:
chai2010
2016-01-18 11:22:04 +08:00
parent 884ada9cd0
commit 9666211cd7
71 changed files with 107 additions and 105 deletions

View File

@@ -6,7 +6,7 @@
在一個程序中有非併發安全的類型的情況下我們依然可以使這個程序併發安全。確實併發安全的類型是例外而不是規則所以隻有當文檔中明確地説明了其是併發安全的情況下你才可以併發地去訪問它。我們會避免併發訪問大多數的類型無論是將變量局限在單一的一個goroutine內還是用互斥條件維持更高級别的不變性都是爲了這個目的。我們會在本章中説明這些術語。
相反導出包級别的函數一般情況下都是併發安全的。由於package級的變量沒法被限製在單一的gorouine所以脩改這些變量“必”使用互斥條件。
相反導出包級别的函數一般情況下都是併發安全的。由於package級的變量沒法被限製在單一的gorouine所以脩改這些變量“必”使用互斥條件。
一個函數在併發調用時沒法工作的原因太多了,比如死鎖(deadlock)、活鎖(livelock)和餓死(resource starvation)。我們沒有空去討論所有的問題,這里我們隻聚焦在競爭條件上。

View File

@@ -54,7 +54,7 @@ func Balance() int {
上面的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 {
@@ -102,7 +102,7 @@ func Withdraw(amount int) bool {
上面這個例子中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也表示成這種形式

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和操作繫統線程之間的技術上的一些區别。