主語言轉成Go了,記錄一些Go的學習筆記與心得,可能有點凌亂。內容來自於《Go程序設計語言》,這本書強烈推薦。編程
(Go中併發編程是使用的Go獨有的goroutine,不能徹底等同於線程,但這不是這篇的重點,下面不作區分了)緩存
在串行程序中,程序中各個步驟的執行順序由程序邏輯決定。好比,在一系列語句中,第一句在第二句以前執行,以此類推。當一個程序中有多個goroutine時,每一個goroutine內部的各個步驟也是按順序執行的,但咱們不能肯定一個goroutine中的事件x與另外一個goroutine中的事件y的前後順序。若是咱們沒法自信地說一個事件確定先於另一個事件,那麼這兩個事件就是併發的。(嗯,換了個角度理解併發,這個定義也確實有道理.架構
關於併發編程會產生的問題,想必諸位都很清楚。諸如不一樣的線程操做相同的數據,形成的數據丟失,不一致,更新失效等等。在Go中關於併發產生的問題,重點能夠討論一下「競態」----在多個goroutine按某些交錯順序執行時程序沒法給出正確的結果。競態對於程序是致命的,由於它們可能會潛伏在程序中,出現頻率很低,極可能僅在高負載環境或者在使用特定的編譯器,平臺和架構時纔出現。這些都使得競態很難再現和分析。併發
數據競態發生於兩個goroutine併發讀寫同一個變量而且至少其中一個是寫入時。從定義出發,咱們有幾種方法能夠規避數據競態。學習
第一種方法--不要修改變量(有點幽默,但也有效。每一個線程都不會去寫數據,天然也不會發生數據競態的問題線程
第二種方法--避免競態的方法是避免從多個goroutine訪問同一個變量.即咱們只容許惟一的一個goroutine訪問共享的資源,不管有多少個goroutine在作別的操做,當他們須要更改訪問共享資源時都要使用同一個goroutine來實現,而共享的資源也被限制在了這個惟一的goroutine內,天然也就不會產生數據競態的問題。這也是Go這門語言的思想之一 ---- 不要經過共享內存來通訊,要經過通訊來共享內存.Go中能夠用chan來實現這種方式.(關於Chan能夠看看筆者前面的博客喲設計
var deposits = make(chan int) //發送存款餘額 var balances = make(chan int) //接收餘額 func Deposit(amount int) {deposits <- amount} func Balance() int {return <- balances} func teller() { var balance int // balance被限制在 teller goroutine 中 for { select { case amount := <-deposits: balance += amount case balances <- balance: } } } func init() { go teller() }
這個簡單的關於銀行的例子,能夠看出咱們把餘額balance限制在了teller內部,不管是更新餘額仍是讀取當前餘額,都只能經過teller來實現,所以避免了競態的問題.協程
這種方式還能夠拓展,即便一個變量沒法在整個生命週期受限於當個goroutine,加以限制仍然能夠是解決併發訪問的好方法。好比一個常見的場景,能夠經過藉助通道來把共享變量的地址從上一步傳到下一步,從而在流水線上的多個goroutine之間共享該變量。在流水線中的每一步,在把變量地址傳給下一步後就再也不訪問該變量了,這樣全部對於這個變量的訪問都是串行的。這中方式有時也被稱爲「串行受限」. 代碼示例以下blog
type Cake struct {state string} func baker(cooked chan <- *Cake) { for { cake := new(Cake) cake.state = "cooked" cooked <- cake // baker再也不訪問cake變量 } } func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake := range cooked { cake.state = "iced" iced <- cake // icer再也不訪問cake變量 } }
第三種避免數據競態的辦法是容許多個goroutine訪問同一個變量,但在同一時間內只有一個goroutine能夠訪問。這種方法稱爲互斥機制。通俗的說,這也就是咱們常在別的地方使用的「鎖」。生命週期
Go中的互斥鎖是由 sync.Mutex提供的。它提供了兩個方法Lock用於上鎖,Unlock用於解鎖。一個goroutine在每次訪問共享變量以前,它都必須先調用互斥量的Lock方法來獲取一個互斥鎖,若是其餘的goroutine已經取走了互斥鎖,那麼操做會一直阻塞到其餘goroutine調用Unlock以後。互斥變量保護共享變量。按照慣例,被互斥變量保護的變量聲明應當緊接在互斥變量的聲明以後。若是實際狀況不是如此,請確認已加了註釋來講明此事.(深有同感,這確實是一個好的編程習慣)
加鎖與解鎖應當成對的出現,特別是當一個方法有不一樣的分支,請確保每一個分支結束時都釋放了鎖。(這點對於Go來講是特別的,一方面,Go語言的思想倡導儘快返回,一旦有錯誤就儘快返回,儘快的recover, 這就致使了一個方法中可能會有多個分支都返回。另外一方面,因爲defer方法,使咱們沒必要在每一個返回分支末尾都添上解鎖或釋放資源等操做,只要統一在defer中處理便可。)針對於互斥鎖,結合咱們前面的銀行的例子的那部分的代碼,咱們來看一個有意思的問題。
//注意,這裏不是原子操做 func withdraw(amount int) bool { Deposit(-amount) if Balance() < 0 { Deposit(amount) return false } return true }
邏輯很簡單,咱們嘗試提現。若是提現後餘額小於0,則恢復餘額,並返回false,不然返回true. 當咱們給Deposit與Balance的內部都加上鎖,來保證互斥訪問的時候,會有一個有意思的問題.首先要說明的是,這個方法是針對它自己的邏輯----可否提現成功,老是能夠正確的返回的。但反作用時,在進行超額提現時,在Deposit與Balance之間,餘額是會下降到0如下的。換成實際一點的狀況就是,你和你媳婦的共享的銀行卡里有10w,你嘗試買一輛法拉利時,致使了你媳婦買一杯咖啡付款失敗了,而且失敗緣由是--餘額不足。這種狀況的根源是,Deposit與Balance兩個方法內的鎖是割裂開的,並非一個原子操做,也就是說,給了別的goroutine的可乘之機。雖然最終餘額方面的數據老是對的,但過程當中也會發送諸如此類的錯誤。那若是咱們用這樣的實現呢:
//注意,這裏是錯誤的實現 func withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return false } return true }
即嘗試給withdraw自己加鎖。固然實際上,這是行不通的。因爲Deposit內部也在加鎖,這樣的寫法最終會致使死鎖。一種改良方式是,分別實現包內可訪問的deposit方法(在調用處外部提供鎖,本身自己無鎖),以及包外能夠訪問的Deposit(本身自己提供了互斥鎖), 這樣,在諸如提現這種須要同時使用更新餘額/查餘額的地方,咱們就可使用deposit來處理,並在提現方法自己提供鎖來保證原子性。
固然,Go也支持讀寫鎖 sync.RWMutex. 關於讀寫鎖就很少bb了,但有一點要注意,只有在大部分goroutine都在獲取讀鎖,而且鎖競爭很激烈時,RWMutex纔有優點,由於RWMutex須要更加複雜的內部記錄工做,因此在競爭不激烈時它比普通的互斥鎖要慢。
另外,書中提到因爲現代計算機自己的多核機制以及Go中協程的實現,致使在一些無鎖的狀況下(且兩個goroutine在不一樣的CPU上執行,每一個CPU都有本身的緩存),可能致使goroutine拿不到最新的值。不過這種方式一來比較極端,二來能夠經過簡單且成熟的模式來避免。----在可能的狀況下,把變量限制在單個goroutine內,對於其餘的變量,採用互斥鎖。 對於這部分感興趣的同窗,能夠去搜一下Go的內存同步,或者直接找《Go程序設計語言》內存同步這一節看一下。