GO 併發

簡介

  • Go 語言支持併發,我們只需要通過 go 關鍵字來開啓 goroutine 即可。

  • goroutine 是輕量級線程也有叫 用戶級線程,協程的,

  • goroutine 的調度是由 Golang 運行時進行管理的。

  • 你可理解爲一段可以異步執行的代碼,一個新的輕量級線程

  • 進程 => 線程 =>協程

爲什麼會有協程的出現

每一個新鮮事物的出現,都是爲了解決某一類問題的,那協程是爲了解決什麼?

這裏用java的線程舉例:

java的線程調度是經過內核的,也就是 Thread 和 kernel thread 是一比一的關係

也就是java 所謂的線程其實就是在內核的系統線程上包了一層java的東西,其中涉及到

用戶態和內核態之間的切換,中間的過程非常複雜,非常耗時

有關用戶態和內核態,可以參考:

https://www.cnblogs.com/zwj-199306231519/articles/16859489.html

這時候有人就開始想辦法,想做出一個,

不經過內核,不涉及到用戶態的切換,但又類似線程的東西,

這就是最開始協程的概念

goroutine 語法格式

go 函數名( 參數列表 )

例如:

go f(x, y, z)

開啓一個新的 goroutine:

f(x, y, z)

Go 允許使用 go 語句開啓一個新的運行期線程(協程), 即 goroutine,以一個不同的、新創建的 goroutine 來執行一個函數。 同一個程序中的所有 goroutine 共享同一個地址空間

兩個例子

例子1:異步

package main

import (
	"fmt"
	"time"
)

func GoRoutine() {
	go func() {
		time.Sleep(10 * time.Second)
	}()
	// 這裏直接輸出,不會等待十秒
	fmt.Println("I am here")
}

func main() {
	GoRoutine()
}

image-20230204140834428

通過上面例子可以知道,go func開啓的方法是異步的

例子2:無序

package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}

image-20230204141114115

通過輸出可以知道,他們之間執行是無序的

通道(channel)簡介

通道(channel)是用來傳遞數據的一個數據結構。

通道可用於兩個 goroutine 之間通過傳遞一個指定類型的值來同步運行和通訊。操作符 <- 用於指定通道的方向,發送或接收。如果未指定方向,則爲雙向通道。

通道(channel) 聲明,創建,賦值

聲明一個通道很簡單,我們使用chan關鍵字即可

var Channel_name chan Type

通道在使用前必須先創建:

ch := make(chan int)

賦值

ch <- v    // 把 v 發送到通道 ch
v := <-ch  // 從 ch 接收數據
           // 並把值賦給 v

例:

package main

import "fmt"

func main() {

	//使用var關鍵字創建通道
	var mychannel chan int
	fmt.Println("channel的值: ", mychannel)
	fmt.Printf("channel的類型: %T ", mychannel)

	// 使用 make() 函數創建通道
	mychannel1 := make(chan int)
	fmt.Println("\nchannel1的值:", mychannel1)
	fmt.Printf("channel1的類型: %T ", mychannel1)
}

image-20230204144227032

通道(channel)的緩衝區

通道的種類分爲:

  • 帶緩衝的
  • 不帶緩衝的

通過 make 的第二個參數指定緩衝區大小:

ch := make(chan int, 100)

channel 帶緩衝

image-20230204142735869

channel 不帶緩衝

image-20230204142939931

案例

package main

import (
	"fmt"
	"time"
)

func main() {
	channelWithoutCache()
	channelWithCache()
}

func channelWithCache() {
	// 帶緩衝,緩衝區爲1
	ch := make(chan string, 1)
	go func() {

		ch <- "Hello, first msg from channel"
		time.Sleep(time.Second)
		ch <- "Hello, second msg from channel"
	}()

	time.Sleep(2 * time.Second)
	msg := <-ch
	fmt.Println(time.Now().String() + msg)
	msg = <-ch
	fmt.Println(time.Now().String() + msg)
	// 因爲前面我們先睡了2秒,所以其實會有一個已經在緩衝了
	// 當我們嘗試輸出的時候,這個輸出間隔就會明顯小於1秒
	// 我電腦上的幾次實驗,差距都在1s以內
}

func channelWithoutCache() {
	// 不帶緩衝
	ch := make(chan string)
	go func() {
		time.Sleep(time.Second)
		ch <- "Hello, msg from channel"
	}()

	// 這裏比較容易寫成 msg <- ch,編譯會報錯
	msg := <-ch
	fmt.Println(msg)
}

image-20230204144904662

通道的遍歷與關閉

Go 通過 range 關鍵字來實現遍歷讀取到的數據,類似於與數組或切片。格式如下:

v, ok := <-ch

如果通道接收不到數據後 ok 就爲 false,這時通道就可以使用 close() 函數來關閉

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		// 將 x 的值寫入通道
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	// range 函數遍歷每個從通道接收到的數據,因爲 c 在發送完 10 個
	// 數據之後就關閉了通道,所以這裏我們 range 函數在接收到 10 個數據
	// 之後就結束了。如果上面的 c 通道不關閉,那麼 range 函數就不
	// 會結束,從而在接收第 11 個數據的時候就阻塞了。
	for i := range c {
		fmt.Println(i)
	}
}

image-20230204161314886

通道的雙向通道和單向通道

通道包括雙向通道和單向通道,這裏雙向通道只的是支持發送和接收的通道,而單向通道是隻能發送或者只能接收的通道。

雙向通道

語法

使用make函數聲明並初始化一個通道:

ch1 := make(chan string, 3)
  • chan 是表示通道類型的關鍵字
  • string 表示該通道類型的元素類型
  • 3 表示該通道的容量爲3,最多可以緩存3個元素值。

一個通道相當於一個先進先出(FIFO)的隊列,使用操作符 <- 進行元素值的發送和接收:

ch1 <- "1"  //向通道ch1發送數據 "1"

接收元素值:

elem1 := <- ch1 // 接收通道中的元素值

首先接收到的元素爲先存入通道中的元素值,也就是先進先出

案例

package main

import "fmt"

func main() {
	str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, len(str1))

	for _, str := range str1 {
		ch1 <- str
	}
	
	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}
}

執行結果:

image-20230204145720786

單向通道

語法

單向通道包括只能發送的通道和只能接收的通道:

// 寫法1
var WriteChan = make(chan<- interface{}, 1) // 只能向通道發送不能接收的
var ReadChan = make(<-chan interface{}, 1) // 只能從通道中接收不能發送的

// 寫法2
ch := make(chan int)
// 聲明一個只能寫入數據的通道類型, 並賦值爲ch
var chSendOnly chan<- int = ch
//聲明一個只能讀取數據的通道類型, 並賦值爲ch
var chRecvOnly <-chan int = ch

// 總結
// chan<- 只能向通道寫入
// <-chan 只能從通道中讀取

單向通道的這種特性可以用來約束函數的輸入類型或者輸出類型

官方案例-time包中的單向通道

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

timer := time.NewTimer(time.Second)

timer的Timer類型定義如下:

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

image-20230204152100597

第 2 行中 C 通道的類型就是一種只能讀取的單向通道。如果此處不進行通道方向約束,一旦外部向通道寫入數據,將會造成其他使用到計時器的地方邏輯產生混亂。

因此,單向通道有利於代碼接口的嚴謹性。

案例

下面這個例子,完整的展示了單向通道的流程

package main

import (
	"fmt"
)

func main() {
	// 默認爲雙向通道
	chan1 := make(chan int)
	chan2 := make(chan int)
	//函數sendChan只允許發送數據,也就是寫入到通道
	go sendChan(chan1)
	// 函數squarer將chan1的數據轉給chan2
	go squarer(chan2, chan1)
	// 函數recvChan只允許接收數據,也就是從通道中讀取
	go recvChan(chan2)
    // 阻塞main,循環隨機監聽通道
	select {}
}

// (in <-chan int)  參數只允許發送數據,不允許接收
func sendChan(in chan<- int) {
	i := 0
	for {
		in <- i
		i++
	}
}

// (out chan<- int)  參數只允許接收數據,不允許發送數據
func recvChan(out <-chan int) {
	for i := range out {
		fmt.Println("out輸出:", i)
	}
}

// (out chan<- int)  參數只允許接收數據,不允許發送數據
// (in <-chan int)  參數只允許發送數據,不允許接收
func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i
	}
	close(out)
}

image-20230204153632236

select 語句

elect 是 Go 中的一個控制結構,類似於 switch 語句。

select 語句只能用於通道操作,每個 case 必須是一個通道操作,要麼是發送要麼是接收。

select 語句會監聽所有指定的通道上的操作,一旦其中一個通道準備好就會執行相應的代碼塊。

如果多個通道都準備好,那麼 select 語句會隨機選擇一個通道執行。如果所有通道都沒有準備好,那麼執行 default 塊中的代碼。

語法

Go 編程語言中 select 語句的語法如下:

select {
  case <- channel1:
    // 執行的代碼
  case value := <- channel2:
    // 執行的代碼
  case channel3 <- value:
    // 執行的代碼

    // 你可以定義任意數量的 case

  default:
    // 所有通道都沒有準備好,執行的代碼
}

以下描述了 select 語句的語法:

  • 每個 case 都必須是一個通道

  • 所有 channel 表達式都會被求值

  • 所有被髮送的表達式都會被求值

  • 如果任意某個通道可以進行,它就執行,其他被忽略。

  • 如果有多個 case 都可以運行,select 會隨機公平地選出一個執行,其他不會執行。

    否則:

    1. 如果有 default 子句,則執行該語句。
    2. 如果沒有 default 子句,select 將阻塞,直到某個通道可以運行;Go 不會重新對 channel 或值進行求值。

案例

不斷地從兩個通道中獲取到的數據,當兩個通道都沒有可用的數據時,會輸出 "no message received"。

package main

import "fmt"

func main() {
	// 定義兩個通道
	ch1 := make(chan string)
	ch2 := make(chan string)

	// 啓動兩個 goroutine,分別從兩個通道中獲取數據
	go func() {
		for {
			ch1 <- "from 1"
		}
	}()
	go func() {
		for {
			ch2 <- "from 2"
		}
	}()

	// 使用 select 語句非阻塞地從兩個通道中獲取數據
	for {
		select {
		case msg1 := <-ch1:
			fmt.Println(msg1)
		case msg2 := <-ch2:
			fmt.Println(msg2)
		default:
			// 如果兩個通道都沒有可用的數據,則執行這裏的語句
			fmt.Println("no message received")
		}
	}
}

image-20230204155551703

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章