天天看點

了解Goroutine

Go語言裡面的并發使用的是Goroutine,Goroutine可以看做一種輕量級的線程,或者叫使用者級線程。與Java的Thread很像,用法很簡單:

go fun(params);

相當于Java的

new Thread(someRunnable).start();

雖然類似,但是Goroutine與Java Thread有着很大的差別。

Java裡的Thread使用的是線程模型的一對一模型,每一個使用者線程都對應着一個核心級線程。

<a href="http://s1.51cto.com/wyfs02/M00/85/8F/wKioL1eomQmQdx94AABaXID_kNA418.png" target="_blank"></a>

上圖有兩個CPU,然後有4個Java thread,每個Java thread其實就是一個核心級線程,由核心級線程排程器進行排程,輪流使用兩個CPU。核心級線程排程器具有絕對的權力,是以把它放到了下面。核心級線程排程器使用公平的算法讓四個線程使用兩個CPU。

Go的Goroutine是使用者級的線程。同樣是4個Goroutine,可能隻對應了兩個核心級線程。Goroutine排程器把4個Goroutine配置設定到兩個核心級線程上,而這兩個核心級線程對CPU的使用由核心線程排程器來配置設定。

與核心級線程排程器相比,Goroutine的排程器與Goroutine是平等的,是以把它和Goroutine放到了同一個層次。排程器與被排程者權力相同,那被排程者就可以不聽話了。一個Goroutine如果占據了CPU就是不放手,排程器也拿它沒辦法。

同樣是下面一段代碼:

1

2

3

4

5

6

<code>void</code> <code>run() {</code>

<code>  </code><code>int</code> <code>a = </code><code>1</code><code>;</code>

<code>  </code><code>while</code><code>(</code><code>1</code><code>==</code><code>1</code><code>) {</code>

<code>   </code><code>a = </code><code>1</code><code>;</code>

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

<code>}</code>

在Java裡,如果起多個這樣的線程,它們可以平等的使用CPU。但是在Go裡面,如果起多個這樣的Goroutine,在啟動的核心級線程個數一定情況下(通常與CPU個數相等),那麼最先啟動的Goroutine會一直占據CPU,其它的Goroutine會starve,餓死,因為它不能主動放棄CPU,不配合别人工作。說到配合工作,那就需要說一下協程(coroutine,可以當做cooperative routine),協程需要互相合作,互相協助,才能正常工作,是以叫做協程。

 協程并不需要一個排程器,它是完全靠互相之間協調來工作的。協程的定義在學術上很抽象,目前實際應用中,協程通常是使用單個核心級線程,用來把異步程式設計中使用的難懂的callback方式改成看上去像同步程式設計的樣子。

比如nodejs是異步單線程事件驅動的,在一段代碼中如果有多次異步操作,比如先調用一個支付系統,得到結果後再更新資料庫,那麼可能需要嵌套使用callback。pay函數是一個調用支付系統的操作,異步送出請求後就傳回,然後等支付完成的事件後觸發第一個回調函數,這個函數是更新資料庫,又是一個異步操作,等這個異步操作完成後,再次觸發傳回更新結果的回調函數。 這裡隻有兩個異步操作,如果多的話,有可能會有很多嵌套。

<code>pay(amount, callback(payamount) {</code>

<code> </code><code>update(payamount, callback(result) {</code>

<code>   </code><code>return</code> <code>result;</code>

<code>})});</code>

而使用協程,可以看上去像是同步操作

7

8

9

10

11

<code>pay(amount){</code>

<code>  </code><code>//異步,立刻傳回</code>

<code>  </code><code>//payamount需要操作完成後才能被指派</code>

<code> </code><code>payamount = dopay(amount);</code>

<code> </code><code>yeild main;</code><code>//把控制權傳回主routine</code>

<code> </code><code>//dopay事件完成後,主routine會調起這個routine,</code>

<code> </code><code>//繼續執行doupdate</code>

<code> </code><code>result=doupdate(payamount); </code>

<code> </code><code>yeild main;  </code><code>//再次把控制權傳回主routine</code>

<code> </code><code>return</code> <code>result;</code>

(以上都是僞代碼)

把原來的各種嵌套callback改成協程,那麼邏輯就會清晰很多。

Goroutine與Coroutine不一樣,開發者并不需要關心Goroutine如何被調起,如何放棄控制權,而是交給Goroutine排程器來管理。開發者不用關心,但是Go語言的編譯器會替你把工作做了,因為Goroutine必須主動交出控制權才能由排程器統一管理。首先我們可以認為寫上面那種死循環而且不調用任何其他函數的Goroutine是沒意義的,如果真在實際應用中寫出這樣的代碼,那開發者不是一個合格的程式員。一個Goroutine總會調用其他函數的,一種調用是開發者自己寫的函數,一種是Go語言提供的API。那編譯器以及這些API就可以做文章了。

比如

<code>  </code><code>int</code> <code>a = </code><code>0</code><code>;</code>

<code>  </code><code>int</code> <code>b = </code><code>1</code><code>;</code>

<code>  </code><code>a = b * </code><code>2</code><code>;</code>

<code>  </code><code>for</code><code>(</code><code>int</code> <code>i = </code><code>0</code><code>; i &lt; </code><code>100</code><code>; i++) {</code>

<code>    </code><code>a = func1(a);</code>

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

那麼編譯器可能會在調用其他函數的地方偷偷加上幾條語句,比如:

<code>   </code><code>//進入排程器,或者以一定機率進入排程器</code>

<code>   </code><code>schedule();  </code>

<code>   </code><code>a = func1(a);</code>

再比如

<code>  </code><code>socket = </code><code>new</code> <code>socket();</code>

<code> </code><code>while</code><code>(buffer = socker.read()) {</code>

<code>  </code><code>deal(buffer);</code>

socker.read()是Go語言提供的一個系統函數,那麼Go語言可能在這裡面加點操作,讀完資料後,進入排程器,讓排程器決定這個Goroutine是否繼續跑。

下面這段Go語言代碼,把核心級線程設成2個,那麼主線程會餓死,而在func1裡加一個sleep就可以了,這樣func1才有機會放棄控制權。

<a href="http://s5.51cto.com/wyfs02/M00/85/91/wKiom1eomsjB8sEdAADH57ysyHw063.png" target="_blank"></a>

當然Go語言的排程器要比這複雜的多。Goroutine與協程還是有差別的,實作原理是一樣的,但是Goroutine的目的是為了實作并發,在Go語言裡,開發者不能建立核心級線程,隻能建立Goroutine,而協程的目的如上面所示,目前比較常見的用途就是上面這個。Go語言适合編寫高并發的應用,因為建立一個Goroutine的代價很低,而且Goroutine切換上下文開銷也很低,與建立核心級線程相比,Goroutine的開銷可能隻是幾十分之一甚至幾百分之一,而且它不占核心空間,每個核心級線程都會占很大的核心空間,能建立的線程數最多也就幾千個,而Goroutine可以很輕松的建立上萬個。

Goroutine底層的實作,在Linux上面是用makecontext,swapcontext,getcontext,setcontext這幾個函數實作的,這幾個系統調用可以實作使用者空間線程上下文的儲存和切換。

本文轉自nxlhero 51CTO部落格,原文連結:http://blog.51cto.com/nxlhero/1835887,如需轉載請自行聯系原作者

上一篇: ffmpeg 安裝
下一篇: RSA那些坑