簡介
-
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()
}
通過上面例子可以知道,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")
}
通過輸出可以知道,他們之間執行是無序的
通道(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)
}
通道(channel)的緩衝區
通道的種類分爲:
- 帶緩衝的
- 不帶緩衝的
通過 make 的第二個參數指定緩衝區大小:
ch := make(chan int, 100)
channel 帶緩衝
channel 不帶緩衝
案例
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)
}
通道的遍歷與關閉
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)
}
}
通道的雙向通道和單向通道
通道包括雙向通道和單向通道,這裏雙向通道只的是支持發送和接收的通道,而單向通道是隻能發送或者只能接收的通道。
雙向通道
語法
使用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)
}
}
執行結果:
單向通道
語法
單向通道包括只能發送的通道和只能接收的通道:
// 寫法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
}
第 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)
}
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 會隨機公平地選出一個執行,其他不會執行。
否則:
- 如果有 default 子句,則執行該語句。
- 如果沒有 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")
}
}
}