1. 程式人生 > 其它 >Go通關09:併發掌握,goroutine和channel宣告與使用!

Go通關09:併發掌握,goroutine和channel宣告與使用!

什麼是程序、執行緒

程序就是一個應用程式的工作空間,比如你開啟的QQ,微信,工作空間包含了該程式執行所需的所有資源。而執行緒是程序中的執行單位,一個程序最少有一個執行緒。

程序與執行緒對比

  • 程序是系統資源分配和排程的最小單位
  • 執行緒是程式執行的最小單位
  • 一個程序由一個或多個執行緒組成,執行緒是程序中程式碼的不同執行路線
  • 程序之間相互獨立,程序中的執行緒共享程式的記憶體空間及資源
  • 執行緒在時間效率和空間效率都比程序要高

協程

協程是一種使用者態的輕量級執行緒,執行緒是CPU來排程,而協程的排程完全是由使用者來控制的。

協程與執行緒對比

  • 一個執行緒可以有多個協程
  • 執行緒、程序都是同步機制,而協程是非同步
  • 協程可以保留上一次呼叫時的狀態,當過程重入時,相當於進入了上一次的呼叫狀態
  • 協程是需要執行緒來承載執行的,所以協程並不能取代執行緒,執行緒是被分割的CPU資源,協程是組織好的程式碼流程

併發、並行

  1. 併發和並行是相對於程序或者執行緒來說的。
  2. 併發是一個或多個CPU對多個程序/執行緒之間的多路複用,通俗講就是CPU輪流執行多個任務,而每個任務都執行一小段,從巨集觀來看就像在同時執行。
  3. 並行必須有多個CPU來提供支援,真正意義上的在同一時刻執行多個程序或執行緒。

Go語言協程

Go中沒有執行緒的概念,只有協程(goroutine),協程相比執行緒更加輕量,上下文切換更快。Goroutine由 Go 自己來排程,我們只管啟用。
goroutine 通過 go

關鍵字來啟動,非常簡單,go 關鍵字後面加一個方法或函式 go function()

package main

import (
	"fmt"
	"time"
)

func main (){
	go fmt.Println("微客鳥窩")
	fmt.Println("我是無塵啊")
	time.Sleep(time.Second) //等待一秒,使goroutine 執行完畢
}

執行結果:

我是無塵啊
微客鳥窩

Channel

channel(通道) 是用來解決多個 goroutine 之間通訊問題的。

在 Go 語言中,提倡通過通訊來共享記憶體,而不是通過共享記憶體來通訊,其實就是提倡通過 channel 傳送接收訊息的方式進行資料傳遞,而不是通過修改同一個變數。所以在資料流動、傳遞的場景中要優先使用 channel,它是併發安全的,效能也不錯。

channel 宣告

ch := make(chan string)

  • 使用 make 函式
  • chan 是關鍵字,表示 channel 型別,chan 是一個集合型別
  • string 表示 channel 裡存放資料的型別

chan 使用

chan 只有傳送和接收兩種操作:

  1. 傳送: <-chan //向chan內傳送資料
  2. 接收: chan-> //從chan中獲取資料

示例:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan string)
	go func(){
		fmt.Println("微客鳥窩")
		ch <- "執行完畢"
	}()

	fmt.Println("我是無塵啊")
	value := <-ch
	fmt.Println("獲取的chan的值為:",value)
}

執行結果

我是無塵啊
微客鳥窩
獲取的chan的值為: 執行完畢

有了 chan 後,我們可以不使用 Sleep 來等 goroutine執行完了,因為接收操作(value := <-ch)會阻塞等待,直到它獲取到值為止。

無緩衝 channel

上面的操作就是一個無緩衝 channel,通道的容量是0,它不能儲存資料,只是起到了傳輸的作用,所以無緩衝 channel 的傳送和接收操作是同時進行的

有緩衝 channel

在宣告的時候,我們可以傳入第二個引數,即channel容量大小,這樣就是建立了一個有緩衝 channel。

//建立一個容量為3的 channel,其內部可以存放3個型別為int的元素
ch := make(chan int,3)
  • 有緩衝 channel 內部有一個佇列
  • 傳送操作是向佇列尾部追加元素,如果佇列滿了,則阻塞等待,直到接收操作從佇列中取走元素。
  • 接收操作是從佇列頭部取走元素,如果佇列為空,則阻塞等待,直到傳送操作向佇列追加了元素。
  • 可以通過內建函式 cap 來獲取 channel 的容量,通過內建函式 len 獲取 channel 中元素個數。
ch := make(chan int,3)
ch <- 1
ch <- 2
fmt.Println("容量為",cap(ch),"元素個數為:",len(ch))
//列印結果:容量為 3 元素個數為: 2

關閉 channel

使用內建函式 close :close(ch)

  • channel 關閉了就不能再向其傳送資料了,否則會引起 panic 異常。
  • 可以從關閉了的 channel 中接收資料,如果沒資料,則接收到的是元素型別的零值。

單向 channel

只能傳送或者只能接收的 channel 為單向 channel。

單向 channel 宣告

只需要在基礎宣告中增加操作符即可:

send := make(ch<- int) //只能傳送資料給channel
receive := make(<-ch int) //只能從channel中接收資料

示例:

package main

import (
	"fmt"
)
//只能傳送通道
func send(s chan<- string){
	s <- "微客鳥窩"
}
//只能接收通道
func receive(r <-chan string){
	str := <-r
	fmt.Println("str:",str)
}
func main() {
	//建立一個雙向通道
	ch := make(chan string)
	go send(ch)
	receive(ch)
}

//執行結果: str: 微客鳥窩

select+channel

select 可以實現多路複用,即同時監聽多個 channel。

  • 發現哪個 channel 有資料產生,就執行相應的 case 分支
  • 如果同時有多個 case 分支可以執行,則會隨機選擇一個
  • 如果一個 case 分支都不可執行,則 select 會一直等待

示例:

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
			fmt.Println("--", i)
		}
	}
}

執行結果:

-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8

此處留作思考題,為何會這樣輸出呢?