1. 程式人生 > 實用技巧 >一天搞懂Go語言(5)——goroutine和通道

一天搞懂Go語言(5)——goroutine和通道

  併發程式設計表現為程式由若干個自主的活動單元組成。go有兩種併發程式設計風格,一種是goroutine和通道,它們支援通訊順序程序(CSP),CSP是一個併發模式,在不同的執行體(goroutine)之間傳遞值,但是變數本身侷限於單一的執行體。還有一種共享記憶體多執行緒的傳統模型,它們和在其他主流語言中使用執行緒類似。

goroutine

  在go裡面,每一個併發執行的活動稱為goroutine,類似於執行緒。當一個程式啟動時候,只有一個goroutine來呼叫main函式,稱為主goroutine,新的goroutine通過go語句進行建立。

f() //呼叫f(),等待它返回
go f() //新建一個呼叫f()的grouting,不用等待

  除了從main返回或者退出程式外,沒有程式化的方法讓一個goroutine來停止另一個,但是有辦法和goroutine通訊來要求它自己停止。

示例

併發時鐘伺服器

package main

import(
	"io"
	"log"
	"net"
	"time"
)

func handleConn(c net.Conn)  {
	defer c.Close()
	for{
		_,err := io.WriteString(c,time.Now().Format("15:04:05\n"))
		if err != nil{
			return //例如,連線斷開
		}
		time.Sleep(1*time.Second)
	}
}

func main()  {
	listener,err:=net.Listen("tcp","localhost:8080")
	if err!=nil{
		log.Fatal(err)
	}
	for{
		conn,err:=listener.Accept() //阻塞,直到有連線請求進來
		if err!=nil{
			log.Print(err) //連線中止
			continue
		}
		handleConn(conn) //一次處理一個連線
	}
}

  當執行這段程式,使用nc命令連線本地8080埠時候,客戶端會顯示每秒從伺服器傳送的時間。使用killall clock1來終止指定名字的程序

  當第二個客戶端需要連結進來的時候必須等第一個客戶端結束,因為伺服器是順序的,一次只能處理一個客戶請求。讓伺服器支援併發只需要一個很小的改變:在呼叫handleConn的地方新增一個go關鍵字,使它在自己的goroutine內執行。

for{
	conn,err:=listener.Accept() //阻塞,直到有連線請求進來
	if err!=nil{
		log.Print(err) //連線中止
		continue
	}
	go handleConn(conn) //併發處理連線
}

  現在多個客戶端可以同時接收到時間。

通道

  如果說goroutine是Go程式併發的執行體,通道就是它們之間的連線,每一個通道是一個具體型別的導管,叫做通道的元素型別。

ch := make(chan int) //ch型別是‘chan int’ ,建立一個無緩衝通道
ch := make(chan int,3) //容量為3的緩衝通道

  像map一樣,通道是一個使用make建立的資料結構的引用。和其他引用型別一樣,零值為nil。通道由主要兩個操作,傳送和接收。

ch <- x //傳送語句
x = <-ch //接收語句
<-ch //接收,丟棄結果

//第三個操作close,將設定一個標誌位來指示這個通道後面沒有值了;
close(ch)

  關閉後的傳送操作將導致宕機,而接收操作將獲取所有已傳送的值。

  使用make建立的通道叫做無緩衝通道,後面還有一個可選引數表示通道的容量,如果為0(預設值),則建立一個無緩衝通道

無緩衝通道

  無緩衝通道,無論是傳送操作還是接收操作都會阻塞,直到一方傳送完畢或接收完畢。這樣會導致兩個goroutine同步化,因此無緩衝通道也叫同步通道。通過通道傳送訊息有一個值,有時候通訊本身以及通訊發生的時間也很重要,當我們強調這方面的時候,往往把訊息稱作事件。當事件沒有攜帶額外訊息,只是單純的同步,我們通過使用一個struct{}元素型別通道強調它,儘管通常使用bool或int。

管道

  多個goroutine通過通道連線起來,一個輸出是另一個的輸入,就組成了管道。下面是三個goroutine,產生整數、求平方、輸出。

package main

import "fmt"

func main()  {
	naturals:=make(chan int)
	squares:=make(chan int)
	
	//counter
	go func() {
		for x:=0; ;x++{
			naturals <- x
		}
	}()
	
	//squares
	go func() {
		for{
			x:=<-naturals
			squares<-x*x
		}
	}()
	
	//printer
	for{
		fmt.Println(<-squares)
	}
}

  如果傳送方沒有資料要傳送,我們可以通過close關閉,所有傳送操作會宕機,後續接收操作會獲取零值。沒有一個直接方式來判斷通道是否關閉,但是有一個接收操作的變種,他產生兩個結果:接收到的通道元素以及一個布林值:true表示成功接收,false表示通道關閉。

//squares
go func() {
	for{
		x,ok:=<-naturals
		if !ok{
			break //通道關閉
		}
		squares<-x*x
	}
	close(squares)//通道讀完關閉
}()

  更為方便的是該語言提供了range迴圈語法以在通道上迭代,接受完最後一個值後關閉迴圈。

func main()  {
	naturals:=make(chan int)
	squares:=make(chan int)

	//counter
	go func() {
		for x:=0; ;x++{
			naturals <- x
		}
		close(naturals)
	}()

	//squares
	go func() {
		for x:=range naturals{
			squares<-x*x
		}
		close(squares)//通道讀完關閉
	}()

	//printer
	for x:=range squares{
		fmt.Println(x)
	}
}

  結束時關閉每一個通道不是必須的,因為go語言可以通過垃圾回收器根據它是否可以訪問來決定是否回收它,而不是根據它是否關閉。(和檔案close操作不一樣,關閉檔案是必須的)。

單向通道

  當一個通道用做函式的形參時,它幾乎總是被有意地限制不能傳送或接收。

chan<-int //只能傳送的通道
<-chan int //只能接收的通道

  因為close操作僅僅在傳送方才能呼叫,所以試圖關閉一個僅能接收的通道在編譯時會報錯

func counter(out chan<-int)  {
	for x:=0;x<100;x++{
		out<-x
	}
	close(out)
}
//...

func main()  {
	naturals:=make(chan int)
	go counter(naturals)
//...
}

  在任何賦值操作中,將雙向通道轉換為單向通道都是允許的,但反過來不行

緩衝通道

  緩衝通道使得傳送方可以無阻塞的傳送緩衝容量大小的資料,接收方相反。所以當緩衝通道滿,傳送操作會阻塞,緩衝通道空,接收操作會阻塞。 

ch := make(chan int,3)

cap(ch) //獲取通道容量
len(ch) //獲取通道元素個數

  向一個沒有goroutine在接收的通道上不斷髮送會發生goroutine洩漏。

select多路複用

  select是Go中的一個控制結構,類似於用於通訊的switch語句。每個case必須是一個通訊操作,要麼是傳送要麼是接收。select隨機執行一個可執行的case。如果沒有 case 可執行,它將阻塞,直到有 case 可執行。一個預設的子句應該總是可執行。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定義任意數量的 case */
    default : /* 可選 */
       statement(s);
}
  • 相關語法:
    • 每個case都必須是一個通訊
    • 所有channel表示式都會被求值
    • 所有被髮送的表示式都會被求值
    • 如果任意某個通訊可以進行,它就執行,其他被忽略
    • 如果有多個case都可以執行,select 會隨機公平地選出一個執行。其他不會執行
    • 如果沒有通道可以執行,則:
      • 如果有default子句,則執行該語句
      • 如果沒有default子句,select 將阻塞,直到某個通訊可以執行;Go不會重新對channel或值進行求值

取消

  有時候我們需要讓一個goroutine停止它當前的任務,一個goroutine無法直接終止另一個,因為這樣會讓所有的共享變數狀態處於不確定狀態。

  當一個通道關閉且已取完所有傳送的值後,接下來的接收操作會立即返回,得到零值。我們可以利用它建立一個廣播機制:不在通道上傳送值,而是關閉它