Golang的goroutine協程和channel通道


一:簡介

因為並發程序要考慮很多的細節,以保證對共享變量的正確訪問,使得並發編程在很多情況下變得很復雜。
但是Go語言在開發並發時,是比較簡潔的。它通過channel來傳遞數據。數據競爭這個問題在golang的設計上就進行了規避了。它提倡用通信的方式實現共享,而不要以共享方式來通信
Go語言用2種手段來實現並發程序,goroutine和channel,其支持順序通信進程(communicating sequential processes),簡稱為CSP。CSP是一種現代的並發編程模型,在這種編程模型中,值會在不同的運行實例(goroutine)中傳遞。

二:Goroutine

在Go語言中,每一個並發的執行單元就叫做goroutine。
每個goroutine都對應一個非常簡單的模型:它是一個並發的執行函數,並且在多個並發的goroutine間,資源是共享的。goroutine非常輕量,創建的開銷很少。

goroutine的用法:
直接在函數前加上一個關鍵字:go。
go func() {}

例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("In main")
	go longSleep()
	go shortSleep()

	fmt.Println("sleep ")
	time.Sleep(10 * 1e9)//ns,符號 1e9 表示 1 乘 10 的 9 次方,e=指數
	fmt.Println("the end of main")
}

func longSleep() {
	fmt.Println("longSleep begin")
	time.Sleep(5 * 1e9)
	fmt.Println("longSleep end")
}

func shortSleep() {
	fmt.Println("shortSleep begin")
	time.Sleep(2 * 1e9)
	fmt.Println("shortSleep end")
}

運行結果:

In main
sleep
longSleep begin
shortSleep begin
shortSleep end
longSleep end
the end of main

main() ,longSleep() 和 shortSleep() 這3個函數都是獨立的處理單元按順序啟動,然后開始並行運行。為了模擬
運算時間的損耗,我們使用了sleep()函數,這個函數可以按照指定時間來暫停函數或協程執行。

如果我們不在main()函數中sleep()較長的時間,那么main() 函數結束時,其他協程運行的程序也會結束。main()程序退出,它不會等待任何其他非main協程的結束。
協程是獨立的處理單元,一旦陸續啟動一些協程,就無法確定他們是什么時候正在開始運行的。

三:通道channel

上面我們講到,協程都是獨立運行的,他們之間沒有通信。
協程可以使用共享變量來通信,但是不建議這么做。在Go中有一種特殊的類型channle通道,可以通過它來進行goroutine之間的通信,可以避免共享內存的坑。channel的通信保證了同步性。
數據通過通道,同一時間只有一個協程可以訪問數據,所以不會出現數據競爭,設計時就是這樣的。

3.1 channel語法

channel也是通過make進行分配的,其返回的是指向底層相關數據結構的引用。

  • 1、基礎語法
var chan1 chan string
chan1 = make(chan string)
//or
chan1 := make(chan string)

//int 
intchan := make(chan int)

//函數也可以
funcchan := chan func()

  • 2、不帶緩沖的channel
var chan2 chan string

chan2 := make(chan string)

chan3 := make(chan string, 0)
  • 3、帶緩沖區的channel
//在make第二個參數加上數字,就變成一個帶緩沖的channel,
//也是一個雙向channel,既可以讀也可以寫
chan3 := make(chan string, 4)
  • 4、單向channel
//只發送的channel,在類型后面加上一個箭頭 <-,只能向channel寫數據
var chan4 chan <-int

chan4 := make(chan <-int)
//只接收的channel,箭頭放在chan前面,只能從channel讀取數據
var chan4 <-chan int

chan4 := make(<-chan int) //初始化

3.2 channel特性

基礎特性

操作 值為 nil 的 channel 被關閉的 channel 正常的 channel
close panic panic 成功關閉
c<- 永遠阻塞 panic 阻塞或成功發送
<-c 永遠阻塞 永遠不阻塞 阻塞或成功接收

happens-before 特性

  1. 無緩沖時,接收 happens-before 發送
  2. 任何情況下,發送 happens-before 接收
  3. close happens-before 接收

3.3 channel用法

3.3.1、無緩沖區

channel無緩沖區,發送方和接收方需要一一配對,不然發送方會一直阻塞,直到數據被接收方取出。
其實無緩沖區channel不管是存消息還是取消息,都會掛起當前goroutine,除非另外一端已經准備好。
無緩沖區的channel永遠不會存數據,只負責數據的流通。

  • 從無緩沖channel取數據,必須要有數據流進來才可以,否則當前協程阻塞
  • 數據流入無緩沖channel, 如果沒有其他goroutine來拿走這個數據,那么當前協程阻塞

注意:
同步的channel不能只在一個協程中發送和接收,因為會被永遠阻塞,數據不能到接收方那里。

package main

import "fmt"

func main() {
	chan1 := make(chan int)

	go func() {
		for d := range chan1 {
			fmt.Println(d)
		}
	}()

	chan1 <- 1 //發送要放在接收協程跑起來后面,因為發送后會阻塞等待接收
	chan1 <- 2
	chan1 <- 3

	close(chan1)
}

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}
func main() {
	s := []int{7, 2, 8, -9, 4, 0}
	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c
	fmt.Println(x, y, x+y)
}

3.3.2、有緩沖區

有緩沖區
channel創建一個緩沖區,如果緩沖區已滿,發送方的主進程或者協程會被阻塞,發送方只能在接收方取走數據后才能從阻塞狀態恢復;如果未滿就不會阻塞;如果為空,接收方的協程會被阻塞。
上面的這種特性,比如可以控制主進程的退出,因為有時我們碰到主協程退出了,其他的子協程還沒有運行完成。

package main

import (
	"fmt"
)

//-------------
var ichan = make(chan int, 3)
var str string

func f() {
	str = "hello world"
	ichan <- 0
}

func main() {
	go f()
	<-ichan  //這里有值,下面才會運行

	fmt.Println(str)
}
package main

import (
	"fmt"
)

func main() {
	chan1 := make(chan int, 3)
	quit := make(chan bool) //阻塞主進程,防止未處理完的子協程

	go func() {
		for d := range chan1 { //如果data的緩沖區為空,這個協程會一直阻塞,除非被channel被close
			fmt.Println(d)
		}
		quit <- true
	}()

	chan1 <- 1
	chan1 <- 2
	chan1 <- 3
	chan1 <- 4
	chan1 <- 5
	close(chan1) //用完需要關閉,否則goroutine會被死鎖,因為上面用range,它是不等到信道關閉是不會結束讀取的
	<-quit       //解除阻塞
}

3.3.3、 for...range

上面有的例子是一個一個的取數據,其實golang還提供了for range 來讀取channel中的數據。

package main

import (
	"fmt"
	"time"
)

func main() {

	go func() {
		time.Sleep(1 * time.Hour)
	}()

	c := make(chan int)
	go func() {
		for i := 0; i < 10; i = i + 1 {
			c <- i
		}
		close(c)//如果把close(c)注釋掉,程序會一直阻塞在for …… range那一行
	}()

	for i := range c {
		fmt.Println(i)
	}

	fmt.Println("end!")
}

//range c 產生的迭代值為channel中發送的值,它會一直迭代直到channel被關閉。
//注意:上面的例子中如果把close(c)注釋掉,程序會一直阻塞在for …… range那一行

3.3.4、select監聽channel

select監測各個channel的數據。
如果有多個channel接收數據,select會隨機選擇一個case來處理。
你還可以給select加上一個default語句,如果沒有case需要處理,那么就會選擇default語句。
多個case情況下,如果沒有default也沒有case需要處理的,那么select會阻塞,只到某個case需要處理。
注意:nil channel 的操作會一直被阻塞,如果沒有default的話,select會一直被阻塞。

package main

import (
	"fmt"
)

func foo(i int) chan int {
	c := make(chan int)
	go func() {
		c <- i
	}()
	return c
}

func main() {
	c1, c2, c3 := foo(1), foo(2), foo(3)

	ichan := make(chan int)
	//開一個goroutine監聽各個channel數據輸出並收集數據到channel
	go func() {
		for {//for語句循環處理select, 如果只有一個select,那么它只會選一個case處理就結束了
			select { //監聽c1,c2,c3流出,並全部流入到ichan
			case v1 := <-c1:
				ichan <- v1
			case v2 := <-c2:
				ichan <- v2
			case v3 := <-c3:
				ichan <- v3
			}
		}
	}()

	//阻塞主協程,取出ichan的數據
	for i := 0; i < 3; i++ {
		fmt.Println(<-ichan) // 從打印來看我們的數據輸出並不是嚴格的1,2,3順序
	}

	fmt.Println("end!")
}

輸出結果:

2
1
3
end!

3.3.5、超時處理

select還有一個應用超時處理的功能。上面說到如果沒有case需要處理,那么select會一直阻塞,這時候我們就可以在一個case下定義一個超時情況,其他case沒有數據處理時,到時間點了這個超時case就會處理了,就不會一直阻塞。
我們用time.After,它返回一個類型為 <-chan time 的單向channel,在指定時間發送一個當前時間給channel

package main

import (
	"fmt"
	"time"
)

func main() {
	chan1 := make(chan string, 1)
	go func() {
		time.Sleep(time.Second * 3)
		chan1 <- "res1"
	}()

	select {
	case res := <-chan1: //3秒之后才會有數據進入槽chan1
		fmt.Println(res)
	case <-time.After(time.Second * 1)://定義超時情況,1秒后超時.這個超時時間比上面的case短,所以先運行這個case
		fmt.Println("timeout 1")
	}
}

輸出:
timeout 1

3.4 close channel

上面的特性我們列舉了close channel的情況。

  • channel已經被關閉

close()掉了,你繼續往里面寫數據,會出現panic。
但是,從這個關閉的channel可以讀出已發送的數據,還可以不斷的讀取零值。
如果是通過range讀取數據,channel關閉后for循環會跳出。

通過i, ok := <-c 可以查看channel的狀態,判斷是零值還是正常讀取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

參考

https://colobu.com/2016/04/14/Golang-Channels/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM