Go part 8 併發編程,goroutine, channel

併發

併發是指的多任務,併發編程含義比較普遍,包含多線程、多進程及分佈式程序,這裏記錄的併發是屬於多線程編程nginx

Go 從語言層面上支持了併發的特性,經過 goroutine 來完成,goroutine 相似於線程,能夠根據須要來建立多個 goroutine 來併發工做編程

goroutine 是在運行時調度完成,而線程是由操做系統調度完成網絡

 

Go 還提供 channel 在多個 goroutine 間進行通訊,goroutine 和 channel 是 Go 秉承 CSP(Communicating Sequential Process)併發模式的重要實現基礎多線程

 

goroutine(輕量級線程)

使用者分配足夠多的任務,系統能自動的把任務分配到 CPU 上,讓這些任務儘可能併發運做,這種機制在 Go 中被稱爲 goroutine閉包

goroutine 的概念相似於線程,Go 程序會自動的將 goroutine 的任務合理的分配給每一個 CPU併發

Go 程序從 main 包的 main() 函數開始,在程序啓動時,就會爲 main() 函數建立一個默認的 goroutine異步

 

建立 goroutine分佈式

爲一個普通函數建立 goroutine 的格式:函數

被調函數的返回值會被忽略性能

go 函數名( 參數列表 )

 

demo:使用 go 關鍵字爲普通函數、匿名函數、閉包函數建立累加器的 goroutine(一個 goroutine 一定對應一個函數)

package main
import (
	"fmt"
	"time"
)

func accumulator(num int){
	for {
		num ++
	    time.Sleep(time.Second)
		fmt.Println(num)
	}
}

func closureAccumulator(num int) func() {
	return func(){
		for {
			num ++
			time.Sleep(time.Second)
			fmt.Printf("閉包函數:%v\n", num)
		}
	}
}

func main(){
	//併發
	go accumulator(0)

	//匿名函數實現併發
	go func() {
		var num int
		for {
			num ++
			time.Sleep(time.Second)
			fmt.Printf("匿名函數:%v\n", num)
		}
	}()

	//閉包實現併發
	go closureAccumulator(0)()

	//不讓 main 包中 goroutine 中止
	for {time.Sleep(time.Second)}
}

運行結果:
1
匿名函數:1
閉包函數:1
閉包函數:2
匿名函數:2
2
閉包函數:3
匿名函數:3
3
...

  

調整併發的運行性能

在 Go 程序運行時(runtime)實現了一個小型的任務調度器,這套調度器的工做原理相似於操做系統調度線程

Go 程序能夠高效的將 CPU 資源分配給每個任務,傳統邏輯中,開發者須要維護線程池中線程與CPU核心數量的關係,一樣,Go 中也能夠經過 runtime.GOMAXPROCS() 函數作到

runtime.GOMAXPROCS(邏輯CPU數量)

這裏的邏輯CPU數量能夠有以下幾種數值:
<1:不修改任何數值
=1:單核心執行
>1:多核併發執行

 

通常狀況下,可使用 runtime.NumCPU() 查詢 CPU 的數量,並使用runtime.GOMAXPROCS() 函數進行設置,例如:

runtime.GOMAXPROCS(runtime.NumCPU())

 

並行與併發的區別

在說併發概念時,總會涉及另一個概念並行, 下面解釋下併發和並行之間的區別

  • 併發(concurrency):把任務在不一樣的時間點交給處理器進行處理。在同一時間點,任務並不會同時運行(作一會數學,而後作一會語文,而後都作好了)
  • 並行(parallelism):把每個任務分配給每個處理器獨立完成。在同一時間點,任務必定是同時運行(眼睛看着屏幕,手指敲鍵盤,這個過程是並行的)

在 GOMAXPROCS 數量與任務數量相等時,能夠作到並行執行,但通常狀況下都是併發執行

 

管道(Chan)

單純的函數併發執行是沒有意義的,函數與函數間須要交換數據才能體現併發執行函數的意義,雖然可使用共享內存進行數據交換,但當多個 goroutine 共存的狀況下容易發生競態問題,爲了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種作法勢必形成性能問題

Go 語言提倡使用通訊的方式代替共享內存,這裏的通訊方法就是使用管道(channel),channel 就是一種隊列同樣的結構,以下圖所示:

 

管道的特性

goroutine 之間經過管道就能夠通訊,在任什麼時候候,同時只能有一個 goroutine 訪問管道進行發送和獲取數據

管道像一個傳送帶或者隊列,遵循先進先出(first in first out)的規則,保證收發數據的順序

 

建立管道

ch1 := make(chan int)                 // 建立一個整型類型的通道
ch2 := make(chan interface{})         // 建立一個空接口類型的通道, 能夠存聽任意格式
type Equip struct{ /* 一些字段 */ }
ch3 := make(chan *Equip)             // 建立Equip指針類型的通道, 能夠存放*Equip

  

使用管道發送和接收數據

1)發送數據

// 建立一個空接口通道
ch := make(chan interface{})
// 將0放入通道中
ch <- 0
// 將hello字符串放入通道中
ch <- "hello"

把數據往通道中發送時,若是沒有 goroutine 進行接收,那麼發送會持續阻塞

Go 程序運行時會智能的發現永遠沒法發送成功的語句,並作出提示 fatal error: all goroutines are asleep - deadlock!

也就是說全部的 goroutine 中的 channel 並無造成發送和接收對應的代碼

 

2)接收數據

管道的收發操做在不一樣的兩個 goroutine 間進行(就像有生產者就必須有消費者同樣),每次只能接收一個元素(相似於往隊列裏面放數據,而後另外一方進行消費)

阻塞接收數據

data := <-ch

demo:

func main(){
	var ch chan int = make(chan int)
	go func (){
		ch <- 1
	}()

	data := <- ch
	fmt.Println(data)
}

 

非阻塞接收數據

data, ok := <-ch

demo:

func main(){
	var ch chan int = make(chan int)
	go func (){
		ch <- 1
	}()

	data, ok := <- ch
	fmt.Println(ok, data)
}

運行結果:
true 1

 

接收任意數據,忽略接收數據

<-ch

demo:

func main(){
	var ch chan int = make(chan int)
	go func (){
		ch <- 1
	}()

	<- ch
}

 

 

循環接收

經過 for range 語句進行多個元素的接收操做:

for data := range ch {
}

demo:

package main
import "fmt"

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

func main(){
	var ch chan int = make(chan int)
	go creater(ch)
	for data := range ch{
		fmt.Print(data)
		if data == 10 {
			break
		}

	}
}

運行結果:
012345678910

 

3)併發打印的例子

demo:main 中的 goroutine 往 chan 中放數據,開啓另一個 goroutine 往文件中寫數據,文件寫入完成以後通知 main 中的 goroutine,最後 main 中的 goroutine 打印 寫入完成

package main
import (
	"fmt"
)

func printer(ch chan int){
	for data := range ch{
		if data == 0 {
			break
		}
		fmt.Println("僞裝寫入到文件,數據是:", data)
	}
	//返回數據輸入端,打印完了
	fmt.Println("寫入完了哈")
	ch <- 1
}

func main(){
    var ch chan int = make(chan  int)
    go printer(ch)
    //輸送數據
    for i:=3; i>=0; i-- {
    	ch <- i
	}

    //接收任意一個數據,若是接收到,表示寫入完成
	<- ch
    fmt.Println("收到了,write complete")
}

運行結果:
僞裝寫入到文件,數據是: 3
僞裝寫入到文件,數據是: 2
僞裝寫入到文件,數據是: 1
寫入完了哈
收到了,write complete

  

管道中的單行道

能夠在聲明的時候約束其操做方向,如 只生產,只消費,這種被約束方向的通道稱爲單向通道

單向通道有利於代碼接口的嚴謹性

單向通道的定義:

1)只生產(消費的時候會報錯)

func main (){
	var chWriteOnly chan<- string = make(chan<- string)

	go func() {
		chWriteOnly <- "hello world ~"
	}()

	fmt.Println(<- chWriteOnly)
}

運行結果:
invalid operation: <-chWriteOnly (receive from send-only type chan<- string)

  

2)只消費(生產的時候會報錯)

func main (){
	var chReadOnly <-chan string = make(<-chan string)

	go func() {
		chReadOnly <- "hello world ~"
	}()

	fmt.Println(<- chReadOnly)
}

運行結果:
invalid operation: chReadOnly <- "hello world ~" (send to receive-only type <-chan string)

定義一個不能生產,只能消費的 chan 是毫無心義的

 

3)time包中的單向通道

time 包中的計時器會返回一個 timer 實例,代碼以下:

timer := time.NewTimer(time.Second)

timer 的 Timer 類型定義以下:

type Timer struct {
    C <-chan Time
    r runtimeTimer
}

C 通道的類型就是一種只能接收的單向通道。若是此處不進行通道方向約束,一旦外部向通道發送數據,將會形成其餘使用到計時器的地方邏輯產生混亂

所以,單向通道有利於代碼接口的嚴謹性

 

帶緩衝的管道

帶緩衝管道和無緩衝管道在特性上是相同的,無緩衝管道能夠看做是長度爲 0 的緩衝管道

爲管道增長一個有限大小的存儲空間造成帶緩衝的管道,在寫入時無需等待獲取方接收便可完成發送過程,並不會阻塞,只有當存儲空間滿時纔會阻塞;同理,若是管道中有數據,接收時將不會發生阻塞,直到通道中沒有數據時,通道纔會阻塞

無緩衝管道是 保證收發過程同步,相似於快遞員給你電話讓你下樓取快遞,整個遞交快遞的過程是同步發生的,你和快遞員不見不散,但這樣作快遞員就必須等待全部人下樓取快遞才能完成全部投遞工做;

帶緩衝的管道,異步收發過程,相似於快遞員將快遞放入快遞櫃,通知用戶來取,效率能夠有明顯的提高

1)建立帶緩衝的管道(相似於定義隊列的長度

func main(){
	var ch chan string = make(chan string, 3)
	ch <- "hello"
	ch <- "how are you"
	ch <- "how do you do"
	//打印管道的長度
	fmt.Println(len(ch))
}

運行結果:
3

  

2)阻塞條件

  • 被生產填滿時,嘗試再次生產數據會發生阻塞
  • 管道爲空時,嘗試消費數據會發生阻塞

爲何要限制管道的長度,而不提供無限長度的管道?

channel 是在兩個 goroutine 間的通訊,使用 goroutine 的代碼必然有一方生產數據,一方消費數據。當生產數據一方的數據供給速度大於消費方的數據處理速度時,若是通道不限制長度,那麼內存將不斷膨脹直到應用崩潰,所以生產者和消費者須要達到一個平衡

 

管道的多路複用(同時生產和消費多個管道的數據)

多路複用是通訊和網絡中的專業術語,一般表示在一個信道上傳輸多路信號或數據流的過程和技術

好比電話就是一種多路複用的設備,能夠在說話的同時聽到對方講話,一條信道上能夠同時接收和發送數據,一樣的,網線、光纖也都是基於多路複用模式來設計的,網線、光纖不只支持同時收發數據,還支持多我的同時收發數據

 

使用管道時,想同時接收多個管道的數據是一件困難的事情,管道在接收數據時,若是沒有數據消費就會發生阻塞,雖然可使用輪詢的方式來處理,但運行性能會很是差

for{
    // 嘗試接收ch1通道
    data, ok := <-ch1
    // 嘗試接收ch2通道
    data, ok := <-ch2
    // 接收後續通道
    …
}

  

Go 中提供了 select 關鍵字(相似於 nginx 中事件通知的機制),能夠同時響應多個管道的操做,select 的每一個 case 都對應一個管道的收發過程,當收發完成時,就會觸發 case 中響應的語句,屢次收發操做在 select 中挑選一個進行響應

 

select 多路複用中能夠接收的樣式
操   做 語句示例
接收任意數據 case <- ch;
接收變量 case d :=  <- ch;
發送數據 case ch <- 100;

 demo:這裏還有點疑問?

package main
import "fmt"

func main() {

	var ch1 chan int = make(chan int, 6)
	var ch2 chan string = make(chan string, 6)
	ch1 <- 100
	ch1 <- 200
	ch2 <- "hello world"

	select {
	case ch1 <- 100:
		fmt.Println("111")

	case strData := <-ch2:
		fmt.Println(strData)

	default:
		fmt.Println("do nothing ")
	}
}

運行結果:
111  或  hello world

 

  

end ~

相關文章
相關標籤/搜索