Go語言並發編程(二)

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

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語言概述

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.