Ready? Go! 下篇:多核並起

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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等基礎資料型別 (Elementary Data Type),也可以是使用者自訂的結構體,介面,甚至可以是通道本身。

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!



相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.