天天看點

Go學習筆記(15)Go并發寫在前面關于并發Go協程與通道在Go中使用協程使用channel在協程間通信使用select來切換協程操作

文章目錄

  • 寫在前面
  • 關于并發
  • Go協程與通道
  • 在Go中使用協程
  • 使用channel在協程間通信
  • 使用select來切換協程操作

寫在前面

    提及Go,大家都會人雲亦雲一句“Go支援高并發,适合使用高并發的場景”,事實也确實如此,Go學習筆記系列也終于到了該介紹下最著名的Go并發的時候了,沒有介紹并發的Go文章是沒有靈魂的哈哈^^

關于并發

    提及并發,很容易聯想到另外一個概念:并行。它們兩個的差別是:

  • 并發主要由切換時間片來實作多個任務“同時”運作
  • 并行是直接通過多核實作多個任務同時運作

    一個并發程式可以隻在一個處理器或核心上運作多個任務,但是某一時間點隻有一個任務在運作;如果運作在多處理器或多核上,就可以在同一時間點有多個任務在運作,才能實作真正的并行,是以,并發程式可以是運作在單處理器(單核)上也可以是運作在多處理器(多核)上。而在Go中,可以設定核數,讓并發程式在多核心上真正并行運作,充分發揮多核計算機的能力

Go協程與通道

    在其它程式設計語言中,實作并發程式往往是使用多線程的技術。在一個程序中有多個線程,它們共享同一個記憶體位址空間。然而使用多線程難以做到準确,尤其是記憶體中資料共享的問題,它們會被多線程以無法預知的方式進行操作,導緻一些無法重制或者随機的結果。多線程解決這個問題的方式是同步不同的線程,對資料加鎖

    但是,這會帶來更高的複雜度,更容易使代碼出錯以及更低的性能,是以這個經典的方法明顯不再适合現代多核/多處理器程式設計

    在Go中,使用協程(goroutines)來實作并發程式:

  • 協程是輕量級的,比線程要更輕,隻需要使用少量的記憶體和資源(每個執行個體4-5k左右的記憶體棧),是以在必要時可以建立大量的協程,實作高并發
  • 協程與作業系統線程之間沒有一對一的關系,協程是根據一個或多個線程的可用性,映射(多路複用)在它們之上的
  • Go多個協程之間使用通道(channel)來同步(它不通過共享記憶體來通信,而是通過通信來共享記憶體),當然Go中也提供了

    sync

    包可以進行傳統的加鎖同步操作,但并不推薦使用

在Go中使用協程

    在Go中使用協程是通過關鍵字go調用一個函數或者方法來實作的:

import (
	"fmt"
	"time"
)
func main(){
	go Goroutine()
	time.Sleep(3 * time.Second)
}
func Goroutine(){
	fmt.Println("start a goroutine")
}
           

    根據上面的代碼,我們定義了一個

Goroutine

的函數,并在主程式中使用

go

關鍵字去調用該函數,進而啟動一個協程去執行

Goroutine

這個函數。在程式中使用

time.Sleep(3*time.Second)

是為了讓主程式延時3s再結束,否則主程式啟動完一個協程後立即退出,我們将沒法看到協程函數中列印的資訊

    新版本的Go(應該是1.8之後)當我們啟動多個協程時,Go将會自動啟動多個核心來并行運作,而在老版本的Go裡需要我們手動設定,手動設定多核心的操作如下:

import (
	"fmt"
	"runtime"
	"time"
)
func main(){
	num := runtime.NumCPU()
	runtime.GOMAXPROCS(num)    //新版本會自動設定
	for i := 0; i < 10; i++ {
		go Goroutine(i)
	}
	time.Sleep(3 * time.Second)
}
func Goroutine(){
	fmt.Println("start a goroutine")
}
           

使用channel在協程間通信

    通道(channel)是一種特殊的類型,可以了解為發送某種其它類型資料的管道,用于在協程之間通信。資料利用通道進行傳遞,在任何時間,通道中的資料隻能被一個協程進行通路,是以不會發生資料競争

  • channel通過

    make

    進行建立,使用

    close

    來關閉
    var ch1 chan string
    ch1 = make(chan string)   //建立一個用于傳遞string類型的通道
               
    如果隻是聲明channel,未初始化,它的值為

    nil

    上面兩行代碼也可以簡寫為

    ch1 := make(chan string)

  • 通道的操作符

    <-

    • 往通道發送資料:

      ch <- int1

      表示把變量

      int1

      發送到通道

      ch

    • 從通道接收資料:

      int2 <- ch

      表示變量

      int2

      從通道

      ch

      中接收資料(如果

      int2

      沒有事先聲明過,則要用

      int2 := <- ch

      )。直接使用

      <-ch

      也可以,也表示從通道中取值,然後該值會被丢棄
    func main(){
    	c := make(chan bool)
    	go func() { //使用匿名函數,閉包,是以可以擷取到外層的channel變量
    		fmt.Println("go go go")
    		c <- true
    	}()
    	<-c         //阻塞,直到從通道取出資料
    }
               
        上面的代碼建立了一個布爾型的channel,主程式啟動協程後就一直阻塞在

    <-c

    那裡等待從通道中取出資料,協程中當列印完資料後,就往通道中發送

    true

    。主程式此時方從通道中取出資料,退出程式。進而不需要手動讓主程式睡眠等待協程完成
  • 大多數情況下channel預設都是阻塞的:從channel取資料一端會阻塞等待channel有資料可以取出才往下執行(如上一段代碼中所示);往channel發送資料一端需要一直阻塞等到資料從channel取出才往下執行。如果把上面那段代碼中往channel中讀取資料的位置調換一下,程式依舊會正常輸出
    func main(){
    	c := make(chan bool)
    	go func() { //使用匿名函數,閉包,是以可以擷取到外層的channel變量
    		fmt.Println("go go go")
    		<-c 
    	}()
    	c <- true    //這裡把資料傳入通道後也會阻塞知道通道中資料被取出        
    }
               
  • 根據需要,channel也可以被設定為有緩存的,有緩存的channel在通道被填滿之前不會阻塞(異步)。上面的程式,如果設定為有緩存的channel,那麼主程式往通道中發送資料之後就直接退出了
    func main(){
    	c := make(chan bool, 1)
    	go func() { //使用匿名函數,閉包,是以可以擷取到外層的channel變量
    		fmt.Println("go go go")
    		<-c 
    	}()
    	c <- true     //這裡往通道發完資料就直接退出了
    }
               
        

    make(chan type, buf)

    這裡

    buf

    是通道可以同時容納的元素的個數,如果容量大于 0,通道就是異步的了:緩沖滿載(發送)或變空(接收)之前通信不會阻塞,元素會按照發送的順序被接收
  • 使用

    for-range

    來操作channel

    for-range

    可以用在通道上,以便從通道中擷取值:
    for v := range ch {
    	fmt.Println(v)
    }
               
        它從指定的通道中讀取資料直到通道關閉才能執行下面的代碼,是以程式必須在某個地方

    close

    該通道,否則程式将死鎖
    func main(){
    	c:=make(chan bool)
    	go func(){
        	fmt.Println("gogogo")
        	c <- true
        	close(c)
    	}()
    	for v := range c{
            fmt.Println(v)
        }
    }
               

此外,關于channel還需要注意:

  • channel可以設定為單向(隻讀或隻寫)或雙向通道(能讀能寫),預設是雙向的
  • channel是引用類型
  • 一個channel隻能傳遞一種類型的資料

使用select來切換協程操作

    使用

select

可以從不同的并發執行的協程中擷取值,它和

switch

語句很類似。

select

可以用來監聽進入通道的資料,也可以向通道發送資料

select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}   
           

    

select

的功能其實就是處理列出的多個通信中的一個

  • default

    語句是可選的,

    fallthrough

    是不允許的,任何一個

    case

    中執行了

    break

    或者

    return

    語句,

    select

    就結束了
  • 如果所有

    case

    的通道都阻塞了,會等待直到其中一個可以處理
  • 如果有多個

    case

    的通道可以處理,會

    随機

    選擇一個處理
  • 如果沒有通道操作可以處理并且寫了

    default

    語句,它就會執行

    default

    語句
  • select

    中使用發送操作并且有

    default

    可以確定發送不被阻塞!如果沒有

    default

    select

    就會一直阻塞
  • select

    也可以設定逾時處理

    下面的代碼是一個類似生産者-消費者的模式,包括了兩個通道和三個協程,其中協程

goroutine1

goroutine2

分别往通道

ch1

ch2

中寫入資料,協程

goroutine3

則通過

select

分别從兩個通道中讀出資料并輸出

func main() {
        ch1 := make(chan int)
        ch2 := make(chan int)

        go goroutine1(ch1)
        go goroutine2(ch2)
        go goroutine3(ch1, ch2)

        time.Sleep(1e9)
}

func goroutine1(ch chan int) {
        for i := 0; ; i++ {
                ch <- i * 2
        }
}

func goroutine2(ch chan int) {
        for i := 0; ; i++ {
                ch <- i + 5
        }
}

func goroutine3(ch1, ch2 chan int) {
        for {
                select {
                case v := <-ch1:
                        fmt.Printf("Received on channel 1: %d\n", v)
                case v := <-ch2:
                        fmt.Printf("Received on channel 2: %d\n", v)
                }
        }
}
           

繼續閱讀