Hike News
Hike News

Golang多線程初識-day27-goroutine通信-管道(channel)

introduction

  • 類似unix中的管道(pipe),或是隊列(queue)
  • channel為引用類型的數據結構
  • 先進先出
  • 線程安全,多個goroutine同時訪問,不需要加鎖
  • channel是有類型的,整數的channel只能存放整數,依此類推
  • 使用make()初始化管道

聲明

1
2
3
4
5
6
var 變量名 chan 類型
var test chan int
var test1 chan string
var test2 chan map[string]string
var test3 chan stu //自定義結構體類型
var test4 chan *stu //亦可傳入指針
  • 通常變量名會希望知道其為管道,因此會將chan結合其他關鍵字作為變量名
    1
    2
    var IntChan chan int
    var StrChan chan string

操作管道

example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

func main(){
//聲明管道
var intChan chan int

//初始化管道
intChan = make(chan int,10)
mapChan := make(chan map[string]string,10)

//往管道內寫入數據
//int
intChan <- 10

//map
a := make(map[string]string,0)
a["Name"] = "Curtis"
mapChan <- a

//關閉管道
close(intChan)
close(mapChan)

//遍歷管道
//int
for i := range intChan {
fmt.Println(i)
}

//map
for i := range mapChan {
fmt.Println(i)
}
}

result

1
2
10
map[Name:Curtis]

example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func main(){

//聲明並初始化管道
UserChan := make(chan user,10)
UserPtrChan := make(chan *user,10)

var User1 user = user{
UserName:"Tom",
UserAge:18,
}

//往管道內寫入數據
UserChan <- User1
UserPtrChan <- &User1

//關閉管道
close(UserChan)
close(UserPtrChan)

//遍歷管道
for i := range(UserChan) {
fmt.Println(i)
}
for i := range(UserPtrChan){
fmt.Println(*i)
}
}
  • 若管道類型為interface{}則可以接受任何類型數據的寫入

result

1
2
{Tom 18}
{Tom 18}

tips

從channel讀取數據

1
2
a = <- testChan
b := <- testChan
  • 賦值記得加上=
  • 聲明並賦值加上:=

從channel寫入數據

1
testChan <- a

channel和goroutine相結合

使用goroutine搭配channel實現讀寫

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"time"
"fmt"
)

var dataChan chan int = make(chan int,1)

func WriteData (i int) {
dataChan <- i
}

func ReadData(){
data := <- dataChan
fmt.Println(data)
}

func main(){
for i:=0;i<10;i++{
go WriteData(i)
}
for i:=0;i<10;i++{
go ReadData()
}
time.Sleep(time.Second*5)
}

result

1
2
3
4
5
6
7
8
9
10
5
3
1
2
6
7
8
4
0
9

阻塞

  • 若是存放進去管道的數據數量超過管道的容量又沒有其他協程取出數據,管道會一直阻塞等待數據被取出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"time"
"fmt"
)

var dataChan chan int = make(chan int,5)

func WriteData (i int) {
dataChan <- i
fmt.Println("input the data",i)
}

func ReadData(){
data := <- dataChan
fmt.Println("get the data:",data)
}

func main(){
for i:=0;i<10;i++{
go WriteData(i)
}
for i:=0;i<10;i++{
time.Sleep(time.Second)
go ReadData()
}
time.Sleep(time.Second*5)
}

result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
input the data 5
input the data 8
input the data 0
input the data 1
input the data 7
get the data: 1
input the data 9
get the data: 0
input the data 6
get the data: 5
input the data 3
get the data: 8
input the data 2
get the data: 7
input the data 4
get the data: 9
get the data: 6
get the data: 3
get the data: 2
get the data: 4
  • 從上述可看到寫入的協程馬上寫入了5組數據,但要再寫入時因為管道已經滿了,必須等待的協程取出數據,才能在寫入數據到管道中

檢測channel是否被關閉

  • 使用close()函數關閉channel
  • 在關閉channel後,管道只可讀(取出)不可再寫入
  • 在取數據時可判斷管道是否已關閉,將最後一個數據取出時便會停止再取出數據
  • 安全的取出數據,才不會造成死循環
    • val,ok := <- channel
    • ok的值返回false時代表已從關閉的管道中取出最後一條數據

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main(){
var intChan chan int
intChan = make(chan int, 10)
for i:=0;i<10;i++{
intChan <- i
}
close(intChan)
for {
var b int

//ok返回管道中是否還有下一條數據
b,ok := <- intChan
if !ok {
break
}
fmt.Println(b)
}
}
  • 要是沒有使用ok判斷已關閉的管道是否還有下一條數據時,for循環會不斷的訪問管道造成死循環

result

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

channel之間的同步

  • 不再使用time.Sleep()等待線程完成任務

example 1

  • 查找1-100的所有質數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package main

    import "fmt"

    func calc(i int,ResultNum chan int,Sync_signal chan bool){
    //退出時傳一個訊號到同步的channel中表示此任務已完成
    defer func(){
    Sync_signal <- true
    }()

    //判斷質數
    for j:=2;j<i;j++{
    if i%j == 0 {
    return
    }
    }
    ResultNum <- i
    return
    }

    func main(){
    //建立接收同步訊號的channel(可以為任何類型的管道)
    //只要最後傳入訊號即可
    Sync := make(chan bool,100)
    ResultNum := make(chan int,100)
    for i:=1;i<=100;i++{
    go calc(i,ResultNum,Sync)
    }
    //上面共開啟100個線程,因此只要能取出100個訊號則代表子線程皆完成任務
    for i:=1;i<=100;i++{
    //沒有變量接收訊號,代表將訊號取出後就直接丟棄
    <- Sync
    }
    //主線程關閉管道
    close(ResultNum)
    //遍歷管道計算的結果
    for v := range(ResultNum){
    fmt.Println(v)
    }
    }
  • 使用range()遍歷管道時,請注意管道必須先關閉(close())否則panic(Dead Lock)

result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
1
7
3
5
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import "fmt"

func sendData(country string, DataChan chan string, Signal chan struct{}){
DataChan <- country

//完成上述的任務後傳入信號
Signal <- struct{}{}
}

func getData(result chan string){
for {
country,ok := <-result
if !ok {
break
}
fmt.Println(country)
}
}

func main(){
country := []string{"Washington","Tripoli","London","Beijing","Tokyo"}

//創建一個存放data的管道
DataChan := make(chan string,len(country))

//創建一個存放線程結束信號的管道,信號型態為空結構體(可自定義為任何形式)
SyncChan := make(chan struct{},len(country))
for _,v := range country {
go sendData(v,DataChan,SyncChan)
}

//取到全部子線程結束的信號,關閉data管道
go func(){
for i:=0;i<len(country);i++ {
<- SyncChan
}
close(DataChan)
}()

//取data
getData(DataChan)
}

result

1
2
3
4
5
Tripoli
Washington
Beijing
London
Tokyo

(補充)管道的只讀與只寫

通常不會用以下的方法直接創建,會直接拿來當參數的類型使用居多

  • 於函數中接受的形式參數類型為 只讀(只寫) 的通道時,可傳入可讀寫的雙向通道作為實體參數 (推薦)

創建可讀可寫管道(一般)

1
2
3
var 變量名 = make(chan int,1)

變量名 := make(chan int,1)

創建只讀的管道

1
2
3
var 變量名 <- chan int

變量名 := make(<-chan int,10)

創建只寫的管道

1
2
3
var 變量名 chan <- int

變量名 := make(chan <- int ,10)

對channel進行select操作

應用1

管道都有固定大小,
實際業務開發中,有可能會有管道空間不夠使用的情況,
此時會一直阻塞,並等待數據被取出,才能再次寫入數據,
若是一直無法將數據取出,會造成之後要寫入的數據無法進入管道,而使得業務停擺

解決方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main(){
var testChan = make(chan int,10)
test2Chan := make(chan int,10)
for i:=1;i<25;i++{
select {
case testChan <- i:

//1.創建多個管道,當第一個管道滿時,存入第二個管道
case test2Chan <- i:

//2.當連多個管道都不夠用時,採用default方法,防止堵塞情況
default :
fmt.Println(i,"can't send to Channel")
}
}
}

result

1
2
3
4
21 can't send to Channel
22 can't send to Channel
23 can't send to Channel
24 can't send to Channel

應用2

管道可能為空的情況下,
有可能仍要持續的取出存放在管道內的數據,
否則會一直阻塞,等待管道中有數據被寫入,才能再次取出數據,
若是一直無數據被寫入,會造成業務停擺

解決方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main(){
testChan := make(chan int, 10)
for i:=1;i<10;i++{
testChan <- i
}
for i:=1;i<15;i++{
select {

//盡可能的取出數據
case v := <-testChan:
fmt.Println("data",v ,"is got")

//無數據時,執行default方法,防止堵塞情況發生
default :
fmt.Println("Channel have no item to get")
}
}
}

result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data 1 is got
data 2 is got
data 3 is got
data 4 is got
data 5 is got
data 6 is got
data 7 is got
data 8 is got
data 9 is got
Channel have no item to get
Channel have no item to get
Channel have no item to get
Channel have no item to get
Channel have no item to get