Go學習筆記(15)Go併發

寫在前面

    提及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可以確保發送不被阻塞!如果沒有defaultselect就會一直阻塞
  • select也可以設置超時處理

    下面的代碼是一個類似生產者-消費者的模式,包括了兩個通道和三個協程,其中協程goroutine1goroutine2分別往通道ch1ch2中寫入數據,協程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)
                }
        }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章