1. 程式人生 > >Ready? Go! 下篇:多核並起(轉)

Ready? Go! 下篇:多核並起(轉)

Google於2009年11月釋出了Go程式語言,旨在同時具備C語言的效率和Python的簡便。今年3月,Go開發組正式釋出了Go語言的第一個穩定發行版:Go version 1,簡稱Go 1。這意味著Go語言本身和它的標準庫已經穩定下來,開發者現在可以將其作為一個穩定的開發平臺,構建自己的應用。我們用兩篇文章介紹Go語言的特性和應用,本文是其中的第二篇。

並行和goroutine

 然而,處理器技術的發展指出,比起[掩蓋了各種並行結構的]單處理器,由多個類似的處理器(各自包含自己的儲存單元)組成的多處理器計算機也許會更加強大,可靠和經濟。 --- C.A.R. Hoare,圖靈獎獲得者,CSP作者,於1978年

20世紀六七十年代,為了彌補處理器的處理能力,平行計算曾一度成為研究熱點。期間不乏優秀的想法,如訊號量(Semaphore),管程(Monitor),鎖(mutex)以及基於訊息傳遞的同步機制。但八十年代起,隨著單核處理器效能飛速提高,學術界迎來了平行計算的黑暗時期。六七十年代的研究成果中,只有早期的一些思想被大規模使用在實際開發中。而七十年代後期的很多成果甚至還沒被大規模應用,就伴隨著平行計算黑暗期的到來,或不溫不火,或被收藏入庫。CSP(Communicating Sequential Processes)便是其中之一。但它優雅簡潔的處理方式卻依然在一些小眾語言中流傳了下來。如今,由於能耗和散熱問題,處理器的發展轉而以多核的方式提高處理器效能。我們再次迎來了曾經面對過的平行計算。這時候,CSP模型逐漸展露頭腳。

CSP的基本思路是基於訊息機制的同步和資料共享。與傳統的鎖同步不同,訊息機制簡化了程式設計,並且可以有效地減少潛在bug。基於CSP模型的語言主要有三個分支:忠於原始CSP設計,以Occam為代表的一支;強調網路和模式,以Erlang為代表的一支;再一個就是強調傳遞訊息的通道(channel),以Squeak,Newsqueak,Alef,Limbo和Go為代表的一支。值得一提的是,第三支的語言中,大部分都是有Rob Pike主持或參與開發的,其中自然也包括Go。

既然說起Go的這一分支是以強調通道(channel)為特色,那麼就先從Go的通道說起。Go的通道是一種資料型別,goroutine可以使用它來傳遞資料。至於goroutine是什麼,之後會詳細討論。此處僅需把它理解為與執行緒類似的執行時結構即可。

定義一個通道,需要指定這個通道上傳遞的資料型別。可以是int,float32,float64等基本資料型別,也可以是使用者自定義的結構體,介面,甚至可以是通道本身。

ch := make(chan int)

這樣,就定義了一個傳遞整數型別的通道。如果要從這個通道中讀取一個值,則可以使用<-操作。類似的,寫入則使用->操作符:

// 從ch中讀取一個值存入i中

i := <- ch

// 向ch中寫入j的值

ch <- j

通道的操作是同步的,一個讀操作只有在真正讀到內容之後,才繼續執行下面的語句;而寫操作則只有在寫入資料被通道另一端讀到,才執行之後的語句。(Go中通道也可以加入快取佇列,在此不多討論)

同時,對於通道,還允許使用for迴圈依次處理來自通道的內容:

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

這個函式的任務就是不斷地從通道中讀取Request結構體的指標,然後呼叫process函式進行處理。

除此以外,還可以使用select對多個通道進行讀寫操作:

func Serve(queue chan *Request,
           quit chan bool) {
    for {
        select {
        case req := <- queue:
            process(r)
        case <- quit:
            return
        }
    }
}

這個函式接受兩個通道作為引數。第一個通道queue用來傳遞各種請求。第二個通道quit則用來發布一條信令,告訴該函式返回。

接下來要說的,就是goroutine。它是一種比執行緒還要輕量的並行結構。在Go程式執行時,一般會並行執行幾個執行緒,然後把goroutine分配到各個執行緒中。當一個goroutine結束或者被阻塞的時候,另外一個goroutine將被排程到被阻塞或結束的goroutine所在的執行緒中。這樣的排程保證了每個執行緒可以有較高的使用率,不必一直處於阻塞狀態。由此省去了很多作業系統排程執行緒而導致上下文切換。按照Go官方的說法,一個Go程式同時執行幾萬到幾十萬個goroutine是非常正常的。

使用一個goroutine也非常簡單,只要在函式呼叫前面加入go就可以了:

go process(r)

這樣,process這個函式就單獨執行在一個goroutine中了。

由此帶來的結果,就是極度地簡化了伺服器端對併發連線的處理。眾所周知,如果讓一個執行緒只處理一個使用者連線,那麼開發起來會非常簡單,但是效率不高;而如果一個執行緒處理多個使用者連線,又無端增加了開發難度。而配合通道使用goroutine則在不增加開發難度的同時,也提高了效率。

考慮這樣一個應用場景:伺服器從網路接收客戶端請求,做一些處理,再把結果返回給客戶。

對於不同的使用者連線,用不同的goroutine處理。定義名為UserConn的結構體來表示一個使用者連線。同時,這個結構體定義了一個叫做ReadRequest的方法,用於從網路讀取使用者的請求;還有一個叫做WriteResponse的方法,用於從網路給使用者傳遞結果。作為一個想象的例子,具體的實現細節在此不詳述。

那麼,對於每個連線,要做的事情大約如此:

func ServeClient(conn *UserConn) {
    ch := make(chan *Response)

    // 建立一個goroutine,
    // 專門用於向用戶傳送結果
    go writeRes(conn, ch)

    for {
        // 讀取一個請求,
        //  判斷型別
        // 如果使用者請求關閉,
        //  則函式返回
        req := conn.ReadRequest()
        switch req.Type {
        case NORMAL_REQUEST:
            go process(req, ch)
        case EXIT:
            return
        }
    }
}


writeRes和process的基本結構大約如下:

func writeRes(conn *UserConn,
             ch chan *Response) {
    for r := range ch {
        conn.WriteResponse(r)
    }
}

func process(req *Request,
            ch chan *Response) {
    res := calculate(req)
    ch <-res
}

通道本身很符合人們對於通訊工具的直覺定義,開發者可以很自然地使用通道在goroutine之間建立各種關係。使用通道和goroutine,每個函式要完成的任務都被單一化,減少了發生錯誤的可能。程式碼中,通過傳遞指標的方式來共享記憶體空間,在每次共享之前,都是以訊息進行同步。這又是一條Go的原則:用傳遞訊息來共享記憶體;而不是用共享記憶體來傳遞訊息。由此簡化了並行程式的開發。

作為一個實用的程式語言,Go並沒有按照CSP原始論文中說的,僅僅提供通道的方式來進行同步。Go在標準庫中也提供了基於鎖,訊號量等傳統同步機制的工具。在以上程式碼中,其實存在著一個潛在bug:ServeClient函式不是在所有執行process的goroutine執行結束後再退出,而是在一收到來自客戶端的退出命令後直接退出的。更合理的操作應該在所有處理該連線的goroutine都退出後再返回。在標準庫中,有一個WaitGroup結構體就可以專門解決等待多個goroutine的問題。在此不詳述。

接下來,就是為每個使用者連線開啟一個goroutine,執行ServeClient函式。前面已經說過,由於goroutine是一種比執行緒還輕量的排程單位,如此數目的goroutine並不會帶來嚴重的效能下降。

由於goroutine和訊息機制簡化了開發,並且Go也鼓勵這樣的設計,開發者會自覺地選擇基於多個goroutine的設計。由此帶來的另一個好處,就是程式在多核系統上的擴充套件性。隨著處理器核數量的增加,如何發掘程式內在的並行結構成了當前開發人員面臨的很大挑戰。而使用Go編寫,基於多個goroutine的設計,往往會天生具備著足夠的並行結構來擴充套件到多核處理器之上。每個goroutine實際都是可以放在一個獨立的處理器上,與其他goroutine並行執行。也就是說,今天為四核處理器寫的程式碼,也許不必修改,就可以執行在未來128核的CPU上,並且同時使用所有的核。

無需配置,直接編譯

如果Go需要一個配置檔案,描述如何編譯和構建Go寫的程式,那就是Go的失敗。 --- Go官方文件

對於make,autoconf,automake等用於指定編譯順序和依賴關係的工具,Go的態度是:開發者在寫程式碼的時候,就留下了關於依賴的足夠資訊,不該要求開發者再單獨寫一份配置檔案,去指明依賴關係和編譯順序。為此,開發者只需要在安裝go工具鏈之後,按照官方文件,配置好一個目錄結構和一個環境變數即可。以後任何安裝Go程式,編譯任何Go程式/庫都只需要幾條簡單的命令就可以了。

對於一個自包含(不依賴任何第三方庫)的程式,只需要在當前目錄下執行go build就會編譯好整個程式。

如果我的程式依賴第三方庫,又該如何呢?很簡單,在程式碼中的import語句裡,寫入第三方庫的在網路中的位置即可。這裡的import和Java/Python中的import的概念一樣,都是引入一個包。

import (
    "fmt"
    "github.com/monnand/goredis"
)

import中引入的第一個包,是fmt,這是標準庫中的包,提供Printf一類的格式化輸入和輸出。第二個引入的包則是位於github上的程式碼庫。它會引入github上,使用者monnand下,goredis這個專案定義的包。

接下來,再呼叫go命令安裝這個庫:

go get github.com/monnand/goredis

這樣,go程式就會自動下載,編譯和安裝這個庫(包括它的依賴)。接下來再使用go build編譯依賴goredis的程式。

除此以外,如果依賴goredis的程式也在github(或其他go支援的版本控制庫)中,那麼只用一條go get命令指明該程式所在的遠端地址就足夠了,go會自己下載安裝各種依賴。除了github,go還支援google code,BitBucket,Launchpad,或者是任何位於其他伺服器上,使用svn,git,Bazzar,Mercurial做版本控制的Go程式/庫。這一切都極大地簡化了開發人員和終端使用者的操作。

再談執行效率

  • Matt: 使用Pat/Go後,比起(原來的)Sinatra/Ruby方案,JSON API節點效率提升了多少?給個估計就可以。

  • Blake: 大約10,000倍

  • Matt: 漂亮!我能引述你的話嗎?

  • Blake: 我再查查,我覺得好像低估了。

--- Matt Aimonetti與Blake Mizerany在推特上的對話。

Go程式的執行效率一直是人們關注的焦點。一方面,Go的語法,型別系統都非常簡單,為編譯器的開發和優化提供了很大空間。另一方面,Go作為靜態編譯型語言,程式碼直接編譯為機器碼,無需中間解釋。

不過倘若在網上搜索一下,就會發現關於Go程式的執行效率,存在著嚴重的兩極分化。一部分測試顯示,Go的程式執行效率非常高,甚至一些方面超過了C++寫的同等程式。另一部分測試則現實,某些方面,Go甚至不如Stackless Python寫的指令碼。

Go編譯器本身雖然還存在很大優化空間,但產生的機器碼效率已經比較高。而標準庫 -- 其中包括各種執行時程式碼,比如垃圾回收,雜湊表等 -- 則還沒有怎麼優化,甚至有些還處於很初級的階段。這是網路上的測試結果存在著嚴重差異的原因之一。另外,作為一個新的語言,開發人員由於對它不熟悉,寫出的程式碼可能存在效能瓶頸,也加大了評測結果的差異。

Go語言的開發者之一,Russ Cox曾在Go的官方部落格上發表了一篇文章。其中使用了某基準測試程式(Benchmark)的程式碼,分別優化了其中的C++測試和Go測試部分。優化後的Go程式執行時間,甚至僅僅是優化後的C++程式執行時間的65.8%!這也從一個側面反應出了Go的潛力。

當前Go語言中,還存在不少缺陷:垃圾回收還處於比較初級的階段,而且對於32位系統的支援還不太完善,一些標準庫的程式碼還有待優化。按照Go官方的說法,未來將會使用完全並行的垃圾回收器,這對於效能來說將會有很大的提高。而隨著Go 1的釋出,Go開發組也會將精力從語法和標準庫的規範,轉移到對編譯器和標準庫的優化上。Go程式的執行效率,目標將會是逼近C++,超越Java。

總結

現在來說,我覺得在系統級開發方面,它(Go)比C++要好上許多。使用它開發更高效,並能使用比C++更簡單的方式解決很多問題。---- Bruce Eckel, 《C++程式設計思想》《Java程式設計思想》作者

Unix創始人Ken Thonpson;UNIX/Plan 9開發者Rob Pike,Russ Cox;memcached作者Brad Fitzpatrick;Java Hotspot編譯器作者之一,Chrome V8引擎作者之一Robert Griesemer;Gold聯結器作者,GCC社群活躍開發人員Ian LanceTaylor……當這樣一群人湊在一起,無論開發什麼,這團隊本身也許已經足以吸引眾人眼球了。而Go作為這樣一個團隊開發出的語言,目前為止還是給不少人帶來了驚喜。

已經有很多公司使用Go開發生產級程式。Rob Pike曾透露過Google內部正逐漸開始使用Go。YouTube則使用Go編寫核心部件,並且將部分程式碼組織成了開源專案vitess。國內包括豆瓣,QBox等公司也已經率先踏入Go語言這個領域。

隨著Go 1的推出,一個穩定的Go語言平臺和開源社群已經形成。對於喜歡嘗試新鮮語言的開發者,Go不失為一個選擇。

=====================================

歡迎關注碼術,一起學習golang!