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

2021-06-21 17:02:59 字數 3769 閱讀 7618

glyph lefkowitz最近寫了一篇 啟蒙文章 ,其中他詳細的說明了一些關於開發高併發軟體的挑戰,如果你開發軟體但是沒有閱讀這篇問題,那麼我建議你閱讀一篇。這是一篇非常好的文章,現代軟體工程應該擁有的豐富智慧型。

從多個花絮中提取,但是如果我斗膽提出主要觀點的總結,其內容就是:搶占式多工和一般共享狀態結合導致軟體開發過程不可管理的複雜性, 開發人員可能更喜歡保持自己的一些理智以此避免這種不可管理的複雜性。搶占式排程對於哪些真正的並行任務是好的,但是當可變狀態通過多併發執行緒共享時,明確的多工合作更招人喜歡 。

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

在過去的一年多,儘管在 heka 上的工作(乙個高效能資料、日誌和指標處理引擎)已大多數使用 go 語言開發。go的亮點之一就是語言本身有一些非常有用的併發原語。但是go的併發效能怎麼樣,需要通過支援本地推理的鼓勵**鏡頭觀察。

並非事實都是好的。所有的goroutine訪問相同的共享記憶體空間,狀態預設可變,但是go的排程程式不保證在上下文選擇過程中的準確性。在單核設定中,go的執行時間進入「隱式協同工作」一類, 在glyph中經常提到的非同步程式模型列表選擇4。 當goroutine能夠在多核系統中並行執行,世事難料。

go不可能保護你,但是並不意味著你不能採取措施保護自己。在寫**過程中通過使用一些go提供的原語,可最小化相關的搶占式排程產生的異常行為。請看下面glyph示例「賬號轉換」**段中go介面(忽略哪些不易於最終儲存定點小數的浮點數)

func transfer(amount float64, payer, payee *account, 

server someservertype) error  

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

var xferchan = make(chan *transfer) 

var errchan = make(chan error) 

func init()

func transferloop()  

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

xferchan <- xfer 

err := <-errchan 

if err == nil   

return err 

}這裡有更多**,但是我們通過實現乙個微不足道的事件迴圈消除併發問題。當**首次執行時,它啟用乙個goroutine執行迴圈。**請求為了此目的而傳遞入乙個新建立的通道。結果經由乙個錯誤通道返回到迴圈外部。因為通道不是緩衝的,它們加鎖,並且通過transfer函式無論多個併發的**請求怎麼進,它們都將通過單一的執行事件迴圈被持續的服務。

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

type account struct

func (a *account) balance() float64

func (a *account) deposit(amount float64)

func (a *account) withdraw(amount float64)

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

func (a *account) withdraw(amount float64)  

log.printf("withdrawing: %f", amount) 

a.balance -= amount 

}不幸的是,這個**患有和我們原來的 transfer 實現相同的問題。併發執行或不幸的上下文切換意味著我們可能以負平衡結束。幸運的是,內部的事件迴圈理念應用在這裡同樣很好,甚至更好,因為事件迴圈 goroutine 可以與每個個人賬戶結構例項很好的耦合。這裡有乙個例子說明這一點:

type account struct

func newaccount(balance float64) (a *account)  

go a.run() 

return 

}func (a *account) balance() float64

func (a *account) deposit(amount float64) error

func (a *account) withdraw(amount float64) error

這個api略有不同,deposit 和 withdraw 方法現在都返回了錯誤。它們並非直接處理它們的請求,而是把賬戶餘額的調整量放入 deltachan,在 run 方法執行時的事件迴圈中訪問 deltachan。同樣的,balance 方法通過阻塞不斷地在事���迴圈中請求資料,直到它通過 balancechan 接收到乙個值。

須注意的要點是上述的**,所有對結構內部資料值得直接訪問和修改都是有事件迴圈觸發的 *within* **來完成的. 如果公共 api 呼叫表現良好並且只使用給出的渠道同資料進行互動的話, 那麼不管對公共方法進行多少併發的呼叫,我們都知道在任意給定的時間只會有它們之中的乙個方法得到處理. 我們的時間迴圈**推理起來更加容易了很多.

該模式的核心是 hek e 的設計. 當heka啟動時,它會讀取配置檔案並且在它自己的go例程中啟動每乙個外掛程式. 隨著時鐘訊號、關閉通知和其它控制訊號,資料經由通道被送入外掛程式中. 這樣就鼓勵了外掛程式作者使用一種想上述事例那樣的 事件迴圈型別的架構 來實現外掛程式的功能.

再次,go不會保護你自己. 寫乙個同其內部資料管理和主題有爭議的條件保持松耦合的heka外掛程式(或者任何架構)是完全可能的. 但是有一些需要注意的小地方,還有go的爭議探測器的自由應用程式,你可以編寫的**其行為可以**,甚至在搶占式排程的門面**中.

原文:

C語言中正確使用const

以前竟然不知道c語言中可以使用const關鍵字,慚愧啊!看到一篇不錯的文章,到此。出處是http www.yesky.com 356 1854856.shtml.基本解釋 const是乙個c語言的關鍵字,它限定乙個變數不允許被改變。使用const在一定程度上可以提高程式的健壯性,另外,在 別人 的時...

C語言中正確使用const

基本解釋 const是乙個c語言的關鍵字,它限定乙個變數不允許被改變。使用const在一定程度上可以提高程式的健壯性,另外,在 別人 的時候,清晰理解const所起的作用,對理解對方的程式也有一些幫助。雖然這聽起來很簡單,但實際上,const的使用也是c語言中乙個比較微妙的地方,微妙在何處呢?請看下...

Go語言中反射的正確使用

介紹 反射是元資料程式設計的一種形式,指的是程式獲得本身結構的一種能力。不同語言的反射模型實現不一樣,本文中的反射,僅僅指的是go語言中的反射模型。反射有兩個問題,在使用前需要三思 go的型別設計上有一些基本原則,理解這些基本原則會有助於你理解反射的本質 多說無用,下面來看示例 複製 如下 pack...