這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
簡介:本文主要是針對一些對於goroutine的“指控”提出我自己的看法,特別是軒脈刃的一篇部落格文章《論go語言中goroutine的使用》提出了goroutine的幾宗罪。實際上goroutine確實有增加程式複雜度而容易導致問題之處,特別是死結;但是另外的一些指控,我認為實際上goroutine是沒有直接責任的。
以下就《論go語言中goroutine的使用》的內容一一提出我的觀點。
第一個指控:goroutine的指標傳遞是不安全的原文:fun main() { request := request.NewRequest() //這裡的NewRequest()是傳遞迴一個typeRequest的指標 gosaveRequestToRedis1(request) gosaveReuqestToRedis2(request) select{}}func saveRequestToRedis1(request*Request){ … request.ToUsers = []int{1,2,3}//這裡是一個賦值操作,修改了request指向的資料結構 … redis.Save(request) return}這樣有什麼問題?saveRequestToRedis1和saveReuqestToRedis2兩個goroutine修改了同一個共用資料結構,但是由於routine的執行是無序的,因此我們無法保證request.ToUsers設定和redis.Save()是一個原子操作,這樣就會出現實際儲存redis的資料錯誤的bug。好吧,你可以說這個saveRequestToRedis的函數實現的有問題,沒有考慮到會是使用goroutine調用。請再想一想,這個saveRequestToRedis的具體實現是沒有任何問題的,它不應該考慮上層是怎麼使用它的。那就是我的goroutine的使用有問題,主routine在開一個routine的時候並沒有確認這個routine裡面的任何一句代碼有沒有修改了主routine中的資料。對的,主routine確實需要考慮這個情況。但是按照這個思路,所以呢?主goroutine在啟用goroutine的時候需要閱讀子routine中的每行代碼來確定是否有修改共用資料??這在實際項目開發過程中是多麼降低開發速度的一件事情啊!
我的觀點:1.函數和調用者直接必須遵循一定的規範或者說約定。這個約定包括:1) 函數簽名。這個在強型別的程式設計語言中可以由編譯器保證。2)語義。就是調用者要根據函數的用途去調用函數,函數也必須實現自己的目的。如果一個讀函數Read實際執行的卻是Write操作,就是語義錯誤。3)附帶資料的許可權控制,包括讀寫權限和線程(goroutine)安全性。比如參數或者返回的資料由誰來負責控制,函數是不是可以寫參數所指向的資料等等。特別在package暴露出來的函數中,對資料的許可權說明就特別重要。一個例子是bytes.Buffer.Next方法,在文檔中很明確地說明它返回的資料只在下次讀寫操作前有效。2.根據1的原則來看上面的例子,就發現saveRequestToRedis1和調用者之間的呼叫慣例並不明確。如果呼叫慣例說明參數指向的資料會被修改,就是調用者的問題;如果呼叫慣例說明參數指向的資料不會被修改,就是函數實現的問題。3.因此,本例子中的問題實際上是呼叫慣例不明確或者沒有遵守的問題,goroutine在這裡是無辜的。
第二個指控:goroutine增加了函數的危險係數原文:上文說,往一個go函數中傳遞指標是不安全的。那麼換個角度想,你怎麼能保證你要調用的函數在函數實現內部不會使用go呢?如果不去看函數體內部具體實現,是沒有辦法確定的。例如我們將上面的典型例子稍微改改func main() { request := request.NewRequest() saveRequestToRedis1(request) saveRequestToRedis2(request) select{}}這下我們沒有使用並發,就一定不會出現這問題了吧?追到函數裡面去,傻眼了:func saveReqeustToRedis1(request*Request) {
…go func() {
…
request.ToUsers = []{1,2,3}
….
redis.Save(request)
}
}我勒個去啊,裡面起了一個goroutine,並修改了request指標指向的對象。這裡就產生了錯誤了。好吧,如果在調用函數的時候,不看函數內部的具體實現,這個問題就無法避免。所以說呢?所以說,從最壞的思考角度出發,每個調用函數理論上來說都是不安全的!試想一下,這個調用函數如果不是自己開發組的人編寫的,而是使用網路上的第三方開原始碼...確實無法想象找出這個bug要花費多少時間。
我的觀點:1.其實這個問題和第一個指控是類似的,實際問題還是關於資料的許可權和goroutine安全性的約定不明確,只是現在是函數調用其他子函數從而本身變成調用者而已。2.關於goroutine安全性舉個例子:database/sql.Stmt的文檔說明就有指出“Stmt is safefor concurrent use by multiple goroutines”。3.那麼,使用網上的第三方庫怎麼辦?我的觀點是如果它的介面文檔簡陋沒有相關的約定說明,建議這樣的庫還是不要用了,不然風險太大了。實際上庫的品質不僅僅包括代碼品質,也包括文檔的品質。
第三個指控:goroutine的濫用陷阱原文:func main() {
gosaveRequestToRedises(request)
}func saveRequestToRedieses(request*Request) {
for _, redis := range Redises{
goredis.saveRequestToRedis(request)
}
}func saveRequestToRedis(request*Request) {
….go func() {
request.ToUsers = []{1,2,3}
…
redis.Save(request)
}()
}神奇啊,go無處不在,好像眨眨眼就在哪裡冒出來了。這就是go的濫用,到處都見到go,但是卻不是很明確,哪裡該用go?為什麼用go?goroutine確實會有效率的提升嗎?c語言的並發比go語言的並發複雜和繁瑣地多,因此我們在使用之前會深思,考慮使用並發獲得的好處和壞處。go呢?幾乎不。
我的觀點:1.對於package暴露出來的函數,必須在文檔(注釋)中明確寫明函數的呼叫慣例。go標準庫就是個比較好的榜樣。2.對於package內部的函數,不需要很明確地寫出呼叫慣例。如果是多人開發同一個package,則開發人員有責任去瞭解被調用函數的預設約定(通過查看函數實現或者簡單的約定說明)。3.在遵守函數約定的前提下,使用goroutine完全不是問題。舉個例子:假設要實現一個排序函數sort,約定是線程不安全的,即不允許把同一個數列在多個goroutine中同時排序。但是我們仍然可以在函數內部使用goroutine:func sort(numbers []int) {
var wg sync.WaitGroupfor i := 0; i < 5;i++ {
wg.Add(1)
go func() {
// 排序子數組
wg.Done()
}()
}wg.Wait()// 合并子數組
}
結論:1.一些看似由goroutine導致的問題其實不應該歸咎於goroutine,那些問題可能是由於不遵守函數呼叫慣例導致的;即使在C/C++裡,不遵守函數呼叫慣例一樣會導致問題。2.packge的匯出函數特別需要明確函數呼叫慣例,否則會導致調用者誤用;而packge內部的函數約定,則需要開發人員自己把控(類比於C++中開發人員對類的內建函式的責任)。3.goroutine會導致問題往往是死結等待等多線程中容易發生的問題。這可以從設計一個良好的設計和良好的代碼架構來減少問題的風險,加強程式碼檢閱也是一個重要的措施。