天天看點

論go語言中goroutine的使用1 goroutine的指針傳遞是不安全的2 goroutine增加了函數的危險系數3 goroutine的濫用陷阱處理方法總結

go中的goroutine是go語言在語言級别支援并發的一種特性。初接觸go的時候對go的goroutine的歡喜至極,實作并發簡便到簡直bt的地步。但是在項目過程中,越來越發現goroutine是一個很容易被大家濫用的東西。goroutine是一把雙面刃。這裡列舉一下goroutine使用的幾宗罪:

1

2

3

4

5

6

7

8

<code>fun main() {</code>

<code>    </code><code>request := request.NewRequest() </code><code>//這裡的NewRequest()是傳遞回一個type Request的指針</code>

<code>    </code><code>go saveRequestToRedis1(request)</code>

<code>    </code><code>go saveReuqestToRedis2(request)</code>

<code>    </code> 

<code>    </code><code>select</code><code>{}</code>

<code>}</code>

非常符合邏輯的代碼:

主routine開一個routine把request傳遞給saveRequestToRedis1,讓它把請求儲存到redis節點1中

同時開另一個routine把request傳遞給saveReuqestToRedis2,讓它把請求儲存到redis節點2中

然後主routine就進入循環(不結束程序)

問題現在來了,saveRequestToRedis1和saveReuqestToRedis2兩個函數其實不是我寫的,而是團隊另一個人寫的,我對其中的實作一無所知,也不想去仔細看内部的具體實作。但是根據函數名,我想當然地把request指針傳遞進入。

好了,實際上saveRequestToRedis1和saveRequestToRedis2 是這樣實作的:

<code>func saveRequestToRedis1(request *Request){</code>

<code>     </code><code>…</code>

<code>     </code><code>request.ToUsers = []</code><code>int</code><code>{1,2,3} </code><code>//這裡是一個指派操作,修改了request指向的資料結構</code>

<code>    </code><code>redis.Save(request)</code>

<code>    </code><code>return</code>

這樣有什麼問題?saveRequestToRedis1和saveReuqestToRedis2兩個goroutine修改了同一個共享資料結構,但是由于routine的執行是無序的,是以我們無法保證request.ToUsers設定和redis.Save()是一個原子操作,這樣就會出現實際存儲redis的資料錯誤的bug。

好吧,你可以說這個saveRequestToRedis的函數實作的有問題,沒有考慮到會是使用go routine調用。請再想一想,這個saveRequestToRedis的具體實作是沒有任何問題的,它不應該考慮上層是怎麼使用它的。那就是我的goroutine的使用有問題,主routine在開一個routine的時候并沒有确認這個routine裡面的任何一句代碼有沒有修改了主routine中的資料。對的,主routine确實需要考慮這個情況。但是按照這個思路,是以呢?主goroutine在啟用go routine的時候需要閱讀子routine中的每行代碼來确定是否有修改共享資料??這在實際項目開發過程中是多麼降低開發速度的一件事情啊!

go語言使用goroutine是想減輕并發的開發壓力,卻不曾想是在另一方面增加了開發壓力。

上面說的那麼多,就是想得出一個結論:

gorotine的指針傳遞是不安全的!!

如果上一個例子還不夠隐蔽,這裡還有一個例子:

<code>fun (</code><code>this</code> <code>*Request)SaveRedis() {</code>

<code>    </code><code>redis1 := redis.NewRedisAddr(</code><code>"xxxxxx"</code><code>)</code>

<code>    </code><code>redis2 := redis.NewRedisAddr(</code><code>"xxxxxx"</code><code>)</code>

<code>    </code><code>go </code><code>this</code><code>.saveRequestToRedis(redis1)</code>

<code>    </code><code>go </code><code>this</code><code>.saveRequestToRedis(redis2)</code>

很少人會考慮到this指針指向的對象是否會有問題,這裡的this指針傳遞給routine應該說是非常隐蔽的。

這點其實也是源自于上面一點。上文說,往一個go函數中傳遞指針是不安全的。那麼換個角度想,你怎麼能保證你要調用的函數在函數實作内部不會使用go呢?如果不去看函數體内部具體實作,是沒有辦法确定的。

例如我們将上面的典型例子稍微改改

<code>func main() {</code>

<code>    </code><code>request := request.NewRequest()</code>

<code>    </code><code>saveRequestToRedis1(request)</code>

<code>    </code><code>saveRequestToRedis2(request)</code>

這下我們沒有使用并發,就一定不會出現這問題了吧?追到函數裡面去,傻眼了:

9

<code>func saveReqeustToRedis1(request *Request) {</code>

<code>           </code><code>…</code>

<code>            </code><code>go func() {</code>

<code>          </code><code>…</code>

<code>          </code><code>request.ToUsers = []{1,2,3}</code>

<code>         </code><code>….</code>

<code>         </code><code>redis.Save(request)</code>

<code>    </code><code>}</code>

我勒個去啊,裡面起了一個goroutine,并修改了request指針指向的對象。這裡就産生了錯誤了。好吧,如果在調用函數的時候,不看函數内部的具體實作,這個問題就無法避免。是以說呢?是以說,從最壞的思考角度出發,每個調用函數理論上來說都是不安全的!試想一下,這個調用函數如果不是自己開發組的人編寫的,而是使用網絡上的第三方開源代碼...确實無法想象找出這個bug要花費多少時間。

看一下這個例子:

10

11

12

13

14

15

16

17

18

19

<code>    </code><code>go saveRequestToRedises(request)</code>

<code>func saveRequestToRedieses(request *Request) {</code>

<code>    </code><code>for</code> <code>_, redis := range Redises {</code>

<code>        </code><code>go redis.saveRequestToRedis(request)</code>

<code>func saveRequestToRedis(request *Request) {</code>

<code>            </code><code>….</code>

<code>                     </code><code>request.ToUsers = []{1,2,3}</code>

<code>                        </code><code>…</code>

<code>                        </code><code>redis.Save(request)</code>

<code>            </code><code>}</code>

神奇啊,go無處不在,好像眨眨眼就在哪裡冒出來了。這就是go的濫用,到處都見到go,但是卻不是很明确,哪裡該用go?為什麼用go?goroutine确實會有效率的提升麼?

c語言的并發比go語言的并發複雜和繁瑣地多,是以我們在使用之前會深思,考慮使用并發獲得的好處和壞處。go呢?幾乎不。

下面說幾個我處理這些問題的方法:

<code>    </code><code>go saveRequestToRedis1(request.Clone())</code>

<code>    </code><code>go saveReuqestToRedis2(request.Clone())</code>

Clone函數需要另外寫。可以在結構體定義之後簡單跟上這個方法。比如:

<code>func (</code><code>this</code> <code>*Request)Clone(){</code>

<code>    </code><code>newRequest := NewRequst()</code>

<code>    </code><code>newRequest.ToUsers = make([]</code><code>int</code><code>, len(</code><code>this</code><code>.ToUsers))</code>

<code>    </code><code>copy(newRequest.ToUsers, </code><code>this</code><code>.ToUsers)</code>

其實從效率角度考慮這樣确實會産生不必要的Clone的操作,耗費一定記憶體和CPU。但是在我看來,首先,為了安全性,這個嘗試是值得的。其次,如果項目對效率确實有很高的要求,那麼你不妨在開發階段遵照這個原則使用clone,然後在項目優化階段,作為一種優化手段,将不必要的Clone操作去掉。這樣就能在保證安全的前提下做到最好的優化。

有兩種思維邏輯會想到使用goroutine:

比如一個伺服器,接收請求,阻塞式的方法是一個請求處理完成後,才開始第二個請求的處理。其實在設計的時候我們一定不會這麼做,我們會在一開始就已經想到使用并發來處理這個場景,每個請求啟動一個goroutine為它服務,這樣就達到了并行的效果。這種goroutine直接按照思維的邏輯來使用goroutine

一個場景是這樣:需要給一批使用者發送消息,正常邏輯會使用

<code>for</code> <code>_, user := range users {</code>

<code>    </code><code>sendMessage(user)</code>

<code> </code> 

但是在考慮到性能問題的時候,我們就不會這樣做,如果users的個數很大,比如有1000萬個使用者?我們就沒必要将1000萬個使用者放在一個routine中運作處理,考慮将1000萬使用者分成1000份,每份開一個goroutine,一個goroutine分發1萬個使用者,這樣在效率上會提升很多。這種是性能優化上對goroutine的需求

按照項目開發的流程角度來看。在項目開發階段,第一種思路的代碼實作會直接影響到後續的開發實作,是以在項目開發階段應該馬上實作。但是第二種,項目中是由很多小角落是可以使用goroutine進行優化的,但是如果在開發階段對每個優化政策都考慮到,那一定會直接打亂你的開發思路,會讓你的開發周期延長,而且很容易埋下潛在的不安全代碼。是以第二種情況在開發階段絕不應該直接使用goroutine,而該在項目優化階段以優化的思路對項目進行重構。

總結下,文章寫了這麼多,并不是想讓你對goroutine的使用産生畏懼,而是想強調一個觀點:

goroutine的使用應該是保守型的。

在你敲下go這兩個字母之前請仔細思考是否應該使用goroutine這柄利刃。

後續

在你看完這篇以後,也建議看看stevewang的這篇吧:

http://blog.sina.com.cn/s/blog_9be3b8f10101dsr6.html

本文轉自軒脈刃部落格園部落格,原文連結:http://www.cnblogs.com/yjf512/archive/2012/06/30/2571247.html,如需轉載請自行聯系原作者

繼續閱讀