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 < </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,如需轉載請自行聯系原作者