在 Go 語言中,如何正確的使用併發

從多個花絮中提取,可是若是我斗膽提出主要觀點的總結,其內容就是:搶佔式多任務和通常共享狀態結合致使軟件開發過程不可管理的複雜性, 開發人員可能更喜歡保持本身的一些理智以此避免這種不可管理的複雜性。搶佔式調度對於哪些真正的並行任務是好的,可是當可變狀態經過多併發線程共享時,明確的多任務合做更招人喜歡 。安全

儘管合做多任務,你的代碼仍有多是複雜的,它只是有機會保持可管理下必定的複雜性。當控制轉移是明確一個代碼閱讀者至少有一些可見的跡象代表事情可能脫離正軌。沒有明確標記每一個新階段是潛在的地雷:「若是這個操做不是原子操做,最後出現什麼狀況?」那麼在每一個命令之間的空間變成無盡的空間黑洞,可怕的Heisenbugs出現bash

在過去的一年多,儘管在Heka上的工做(一個高性能數據、日誌和指標處理引擎)已大多數使用GO語言開發。Go的亮點之一就是語言自己有一些很是有用的併發原語。可是Go的併發性能怎麼樣,須要經過支持本地推理的鼓勵代碼鏡頭觀察。架構

並不是事實都是好的。全部的Goroutine訪問相同的共享內存空間,狀態默承認變,可是Go的調度程序不保證在上下文選擇過程當中的準確性。在單核設置中,Go的運行時間進入「隱式協同工做」一類, 在Glyph中常常提到的異步程序模型列表選擇4。 當Goroutine可以在多核系統中並行運行,世事難料。併發

Go不可能保護你,可是並不意味着你不能採起措施保護本身。在寫代碼過程當中經過使用一些Go提供的原語,可最小化相關的搶佔式調度產生的異常行爲。請看下面Glyph示例「帳號轉換」代碼段中Go接口(忽略哪些不易於最終存儲定點小數的浮點數)app

func Transfer(amount float64, payer, payee *Account, 
    server SomeServerType) error { 
    if payer.Balance() < amount { 
        return errors.New("Insufficient funds") 
    } 
    log.Printf("%s has sufficient funds", payer) 
    payee.Deposit(amount) 
    log.Printf("%s received payment", payee) 
    payer.Withdraw(amount) 
    log.Printf("%s made payment", payer) 
    server.UpdateBalances(payer, payee) // Assume this is magic and always works. 
    return nil 
} 
複製代碼

這明顯的是不安全的,若是從多個goroutine中調用的話,由於它們可能併發的從存款調度中獲得相同的結果,而後一塊兒請求更多的已取消調用的存款變量。最好是代碼中危險部分不會被多goroutine執行。在此一種方式實現了該功能:異步

type transfer struct { 
    payer *Account 
    payee *Account 
    amount float64 
} 
var xferChan = make(chan *transfer) 
var errChan = make(chan error) 
func init() { 
    go transferLoop() 
} 
func transferLoop() { 
    for xfer := range xferChan { 
        if xfer.payer.Balance < xfer.amount { 
            errChan <- errors.New("Insufficient funds") 
            continue 
        } 
        log.Printf("%s has sufficient funds", xfer.payer) 
        xfer.payee.Deposit(xfer.amount) 
        log.Printf("%s received payment", xfer.payee) 
        xfer.payer.Withdraw(xfer.amount) 
        log.Printf("%s made payment", xfer.payer) 
        errChan <- nil 
    } 
} 
func Transfer(amount float64, payer, payee *Account, 
    server SomeServerType) error { 
    xfer := &transfer{ 
        payer: payer, 
        payee: payee, 
        amount: amount, 
    } 
    xferChan <- xfer 
    err := <-errChan 
    if err == nil  { 
        server.UpdateBalances(payer, payee) // Still magic. 
    } 
    return err 
} 
複製代碼

這裏有更多代碼,可是咱們經過實現一個微不足道的事件循環消除併發問題。當代碼首次執行時,它激活一個goroutine運行循環。轉發請求爲了此目的而傳遞入一個新建立的通道。結果經由一個錯誤通道返回到循環外部。由於通道不是緩衝的,它們加鎖,而且經過Transfer函數不管多個併發的轉發請求怎麼進,它們都將經過單一的運行事件循環被持續的服務。函數

上面的代碼看起來有點彆扭,也許吧. 對於這樣一個簡單的場景一個互斥鎖(mutex)也許會是一個更好的選擇,可是我正要嘗試去證實的是能夠向一個go例程應用隔離狀態操做. 即便稍稍有點尷尬,可是對於大多數需求而言它的表現已經足夠好了,而且它工做起來,甚至使用了最簡單的帳號結構實現:oop

type Account struct { 
    balance float64 
} 
func (a *Account) Balance() float64 { 
    return a.balance 
} 
func (a *Account) Deposit(amount float64) { 
    log.Printf("depositing: %f", amount) 
    a.balance += amount 
} 
func (a *Account) Withdraw(amount float64) { 
    log.Printf("withdrawing: %f", amount) 
    a.balance -= amount 
} 
複製代碼

不過如此笨拙的帳戶實現看起來會有點天真. 經過不讓任何大於當前平衡的撤回操做執行,從而讓帳戶結構自身提供一些保護也許更起做用。那若是咱們把撤回函數變成下面這個樣子會怎麼樣呢?性能

func (a *Account) Withdraw(amount float64) { 
    if amount > a.balance { 
        log.Println("Insufficient funds") 
        return 
    } 
    log.Printf("withdrawing: %f", amount) 
    a.balance -= amount 
} 
複製代碼

不幸的是,這個代碼患有和咱們原來的 Transfer 實現相同的問題。併發執行或不幸的上下文切換意味着咱們可能以負平衡結束。幸運的是,內部的事件循環理念應用在這裏一樣很好,甚至更好,由於事件循環 goroutine 能夠與每一個我的帳戶結構實例很好的耦合。這裏有一個例子說明這一點:ui

type Account struct { 
    balance float64 
    deltaChan chan float64 
    balanceChan chan float64 
    errChan chan error 
} 
func NewAccount(balance float64) (a *Account) { 
    a = &Account{ 
        balance:     balance, 
        deltaChan:   make(chan float64), 
        balanceChan: make(chan float64), 
        errChan:     make(chan error), 
    } 
    go a.run() 
    return 
} 
func (a *Account) Balance() float64 { 
    return <-a.balanceChan 
} 
func (a *Account) Deposit(amount float64) error { 
    a.deltaChan <- amount 
    return <-a.errChan 
} 
func (a *Account) Withdraw(amount float64) error { 
    a.deltaChan <- -amount 
    return <-a.errChan 
} 
func (a *Account) applyDelta(amount float64) error { 
    newBalance := a.balance + amount 
    if newBalance < 0 { 
        return errors.New("Insufficient funds") 
    } 
    a.balance = newBalance 
    return nil 
} 
func (a *Account) run() { 
    var delta float64 
    for { 
        select { 
        case delta = <-a.deltaChan: 
            a.errChan <- a.applyDelta(delta) 
        case a.balanceChan <- a.balance: 
            // Do nothing, we've accomplished our goal w/ the channel put. } } } 複製代碼

這個API略有不一樣,Deposit 和 Withdraw 方法如今都返回了錯誤。它們並不是直接處理它們的請求,而是把帳戶餘額的調整量放入 deltaChan,在 run 方法運行時的事件循環中訪問 deltaChan。一樣的,Balance 方法經過阻塞不斷地在事件循環中請求數據,直到它經過 balanceChan 接收到一個值。

須注意的要點是上述的代碼,全部對結構內部數據值得直接訪問和修改都是有事件循環觸發的

within
代碼來完成的。若是公共 API 調用表現良好而且只使用給出的渠道同數據進行交互的話, 那麼無論對公共方法進行多少併發的調用,咱們都知道在任意給定的時間只會有它們之中的一個方法獲得處理。咱們的時間循環代碼推理起來更加容易了不少。

該模式的核心是 Heke 的設計. 當Heka啓動時,它會讀取配置文件而且在它本身的go例程中啓動每個插件. 隨着時鐘信號、關閉通知和其它控制信號,數據經由通道被送入插件中. 這樣就鼓勵了插件做者使用一種想上述事例那樣的 事件循環類型的架構 來實現插件的功能.

再次,GO不會保護你本身. 寫一個同其內部數據管理和主題有爭議的條件保持鬆耦合的Heka插件(或者任何架構)是徹底可能的。可是有一些須要注意的小地方,還有Go的爭議探測器的自由應用程序,你能夠編寫的代碼其行爲能夠預測,甚至在搶佔式調度的門面代碼中。

英文原文:Sane Concurrency with Go

譯文連接:http://www.oschina.net/translate/sane-concurrency-with-go

相關文章
相關標籤/搜索