這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言的並發編程主要通過goroutine和channel實現的。以下為學習筆記,僅供參考。歡迎指正。
一、goroutine
(一)goroutine的理解
對於初學者,goroutine直接理解成為線程就可以了。當使用go關鍵詞調用函數時,啟動一個goroutine的時候,就相當於啟了一個線程,執行這個函數。
然而實際上一個goroutine並不是一個線程,是比線程還要小的調度單位。預設所有的goroutines在一個線程裡跑,而且goroutines線上程中會是一個一個地執行。如果線程阻塞了,則被分配到閒置線程。
(二)goroutine的使用
使用非常簡單,在函數前增加一個go
例:go f(a,b)//開啟後,不等待其結束,主線程繼續執行。
PS:要注意的是一個goroutine開啟後,若不等其執行,main(主goroutine)中將繼續執行下一步,那麼主線程一結束,goroutine中的程式就不會執行了。如何解決?代碼如下:
func saySomething(str string) { for i := 0; i<5; i++ { time.Sleep(time.Millisecond * 1000) fmt.Println(str) }}func main() { // 啟動一個goroutine線程 go saySomething("Hello") saySomething("World")}
這裡為什麼要sleep? 是為了等go saySomething(“Hello”)處理完成。
好了,這裡就出來了一個需求:如果要人為設定一個休眠的時間,非常地不方便,需要使一個goroutine結束後自動向主線程傳輸資料,告訴主線程這個goroutine已經結束了。這裡就引進了channel的概念。
二、channel
(一)channel概念
簡單來說就是,主線程告訴大家你開goroutine可以,但是我在我的主線程開了一個通道,你做完了你要做的事情之後,往通道裡面塞個東西告訴我你已經完成了,我再結束主線程。
(二)channel使用
1、和map一樣,通道是參考型別,用make 分配記憶體。如果調用make時提供一個可選的整數參數,則該通道就會被分配相應大小的緩衝區。緩衝區大小預設為0,對應於無緩衝通道或者同步通道。
ci := make(chan int) // 無緩衝整數通道cs := make(chan *os.File, 100) // 緩衝的檔案指標通道
2、通道可用來讓正在啟動並執行goroutine等待排序完成。確保(goroutine)相互都處於已知狀態。
-往channel中插入資料的操作
c <- 1
-從channel中輸出資料
<- c
程式碼範例:
c := make(chan int) // Allocate a channel.// 在goroutine中啟動排序,當排序完成時,通道上發出訊號go func() { list.Sort() c <- 1 // 發送一個訊號,值是多少無所謂。}()doSomethingForAWhile()<-c // 等待排序完成,丟棄被發送的值。
收信者(receivers)在收到資料前會一直被阻滯。如果通道是非緩衝的,則發信者(sender)在收信者接收到資料前也一直被阻滯。如果通道有緩衝區,發信者只有在資料被填入緩衝區前才被阻滯;如果緩衝區是滿的,意味著寄件者要等到某個收信者取走一個值。
(三)channel限制輸送量
緩衝的通道可以象號誌一樣使用,比如用來限制輸送量。在下面的例子中,進入的請求被傳遞給handle,handle發送一個值到通道,接著處理請求,最後從通道接收一個值。通道緩衝區的大小限制了並發調用process的數目。
var sem = make(chan int, MaxOutstanding)func handle(r *Request) { sem <- 1 // 等待隊列緩衝區非滿 process(r) // 處理請求,可能會耗費較長時間. <-sem // 請求處理完成,準備處理下一個請求}func Serve(queue chan *Request) { for { req := <-queue go handle(req) //不等待handle完成 }}
通過啟動固定數目的handle goroutines也可以實現同樣的功能,這些goroutines都從請求通道中讀取請求。Goroutines的數目限制了並發調用process的數目。Serve函數也從一個通道中接收退出訊號;在啟動goroutines後,它處於阻滯狀態,直到接收到退出訊號:
func handle(queue chan *Request) { for r := range queue { process(r) }}func Serve(clientRequests chan *clientRequests, quit chan bool) { // 啟動請求處理 for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // 等待退出訊號}
(四)通過通道傳輸通道
Go最重要的特性之一就是: 通道, 通道可以像其它類型的數值一樣被分配記憶體並傳遞。此特性常用於實現安全且並行的去複用(demultiplexing)。
前面的例子中,handle是一個理想化的處理請求的函數,但是我們沒有定義它所能處理的請求的具體類型。如果該類型包括了一個通道,每個用戶端就可以提供自己方式進行應答
type Request struct { args []int f func([]int) int resultChan chan int}
用戶端提供一個函數、該函數的參數以及一個請求對象用來接收應答的通道
func sum(a []int) (s int) {for _, v := range a { s += v}return}request := &Request{[]int{3, 4, 5}, sum, make(chan int)}// 發送請求clientRequests <- request// 等待響應.fmt.Printf("answer: %d\n", <-request.resultChan)
在伺服器端,處理請求的函數是
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) }}
顯然要使這個例子更為實際還有很多工作要做,但這是針對速度限制、並行、非阻滯RPC系統的架構,而且其中也看不到互斥(mutex)的使用。
(五)並行
並行思想的一個應用是利用多核CPU進行並行計算。如果計算過程可以被分為多個片段,則它可以通過這樣一種方式被並行化:在每個片段完成後通過通道發送訊號。
此處上篇文章已經介紹過了,詳見 Go語言並發編程(一)
(六)同步工具sync.WaitGroup
設定一個變數作為同步工具。這是防止主Goroutine過早的被運行結束的有效手段之一。對這個變數的聲明和初始化的代碼如下:
var waitGroup sync.WaitGroup // 用於等待一組操作執行完畢的同步工具。waitGroup.Add(3) // 該組操作的數量是3。numberChan1 := make(chan int64, 3) // 數字通道1。numberChan2 := make(chan int64, 3) // 數字通道2。numberChan3 := make(chan int64, 3) // 數字通道3
標識符sync.WaitGroup代表了一個類型。該類型的聲明存在於程式碼封裝sync中,類型名為WaitGroup。另外,上面的第二條語句進行了一個“加3”的操作,意味著我們將要後面啟用三個Goroutine,或者說要並發的執行三個go函數。
先來看第一個go函數:數字過濾函數,過濾掉不能被2整除的數字。
go func() { // 數字過濾函數1。 for n := range numberChan1 { // 不斷的從數字通道1中接收數字,直到該通道關閉。 if n%2 == 0 { // 僅當數字可以被2整除,才將其發送到數字通道2. numberChan2 <- n } else { fmt.Printf("Filter %d. [filter 1]\n", n) } } close(numberChan2) // 關閉數字通道2。 waitGroup.Done() // 表示此操作完成。進行相應的“減1”}()
數字過濾函數2代碼與上述類似,過濾掉不能被5整除的數字。如下:
go func() { // 數字過濾函數2。 for n := range numberChan2 { // 不斷的從數字通道2中接收數字,直到該通道關閉。 if n%5 == 0 { // 僅當數字可以被5整除,才將其發送到數字通道3. numberChan3 <- n } else { fmt.Printf("Filter %d. [filter 1]\n", n) } } close(numberChan3) // 關閉數字通道3。 waitGroup.Done() // 表示此操作完成。進行相應的“減1”}()
如此一來,數字過濾函數1和2就經由數字通道2串聯起來了。請注意,不要忘記在數字過濾函數2中的for語句後面添加對數字通道numberChan3的關閉操作,以及調用waitGroup變數的Done方法。
go func() { // 數字輸出函數。 for n := range numberChan3 { // 不斷的從數字通道3中接收數字,直到該通道關閉。 fmt.Println(n) // 列印數字。 } waitGroup.Done() // 表示此操作完成。並“減1”。}()
然後啟用這一過濾數位流程。具體的啟用方法是,向數字通道numberChan1發送數字。在上述代碼後加入代碼如下:
for i := 0; i < 100; i++ { // 先後向數字通道1傳送100個範圍在[0,100)的隨機數。 numberChan1 <- rand.Int63n(100)}close(numberChan1) // 數字發送完畢,關閉數字通道1。對通道的關閉並不會影響到對已存於其中的數位接收操作。
為了能夠讓這個流程能夠被完整的執行,我們還需要在最後加入這樣一條語句:
waitGroup.Wait() // 等待前面那組操作(共3個)的完成。
對waitGroup的Wait方法的調用會一直被阻塞,直到前面三個go函數中的三個waitGroup.Done()語句(即那三個“減1操作”)都被執行完畢。也就是當waitGroup裡的數量由3減到0時,才能讓對waitGroup.Wait()語句的執行從阻塞中恢複並完成。
參考文章:go語言學習筆記之並發編程
Go並發編程之Go語言概述
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。