Ready? Go! 下篇:多核並起

Google於2009年11月發佈了Go編程語言,旨在同時具有C語言的效率和Python的簡便。今年3月,Go開發組正式發佈了Go語言的第 一個穩定發行版:Go version 1,簡稱Go 1。這意味着Go語言自己和它的標準庫已經穩定下來,開發者如今能夠將其做爲一個穩定的開發平臺,構建本身的應用。咱們用兩篇文章介紹Go語言的特性和應 用,本文是其中的第二篇。 html

上一期介紹了Go語言的部分語法和類型系統。本篇重點介紹Go語言對於並行的處理和go工具鏈。本文中的完整代碼能夠在github上找到。 git

並行和goroutine

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

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

CSP的基本思路是基於消息機制的同步和數據共享。與傳統的鎖同步不一樣,消息機制簡化了程序設計,而且能夠有效地減小潛在bug。基於CSP模型的語言主要有三個分支:忠於原始CSP設計,以Occam爲表明的一支;強調網絡和模式,以Erlang爲表明的一支;再一個就是強調傳遞消息的信道(channel),以SqueakNewsqueakAlefLimboGo爲表明的一支。值得一提的是,第三支的語言中,大部分都是有Rob Pike主持或參與開發的,其中天然也包括Go。 redis

既然提及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不失爲一個選擇。

相關文章
相關標籤/搜索