Golang: channel

Golang: channel

Go语言以他的原生支持高并发而闻名。一个经典的话就是:**Do not communicate by sharing memory; instead, share memory by communicating.**所以这篇就探索一下channel。

Goroutine(协程)

它是一种轻量级的线程,不是操作系统的线程。与其他Go协程并发地运行在同一地址空间的函数。

这里暂且将他理解为一个轻量级的线程(因为他的栈大小每个线程不一样,相比于OS线程固定的2M来说,Goroutine一般为2KB,但是最大可以是1GB)

使用方法就是在函数前加上go这个关键字就好。

以下是知乎上对goroutine总结的三个陷阱(bak):

在golang中,goroutine是有三个主要的陷阱:

  1. goroutine leaks

  2. data race

  3. incomplete work

对于1和3的情况:Never start a goroutine without knowing how it will stop.

对于2:Don’t communicate by sharing memory, share memory by communicating.

遵循以上能尽量避免以上三个问题的发生

再直白一点就是:

  1. 每当使用go启动一个goroutine时一定要注意它是否能正常结束

  2. 多个goroutine同时操作同一个变量(communicate by sharing memory),会有数据竞争的问题,尽量不要用这种方式;而推荐用传递共享方式,一个goroutine处理完了以后传递给另一个goroutine继续处理(share memory by communicating)

Channel

不同协程之间的通信需要管道。

创建方式

1
2
3
ci := make(chan int)            // 整数无缓冲信道
cj := make(chan int, 0) // 整数无缓冲信道
cs := make(chan *os.File, 100) // 指向文件的指针的缓冲信道

阻塞情况

  • 对于接收者,在收到数据之前会一直阻塞
  • 对于发送者来说
    • 如果无缓冲区。在接收者收到值之前,发送者阻塞
    • 如果有缓冲区
      • 如果缓冲区未满,发送者不阻塞
      • 如果缓冲区满,发送者阻塞直到数据被复制到缓冲区,即用户取出至少一个数据之后,解除阻塞

管道类型

  • 双向管道
  • 只读管道
  • 只写管道

定义如下:

1
2
3
ch1 := make(chan int) //双向管道
ch2 := make(<-chan int) //只读
ch3 := make(chan<- int) //只写

双向管道

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"
"time"
)

func goroutineA(a chan int) {
data := 3
a <- data
fmt.Println("goroutine A Send data: ", data)

receivedData, ok := <-a
if ok {
fmt.Println("goroutine A received data: ", receivedData)
}

close(a)

}

func goroutineB(b chan int) {
data := 1
val, ok := <-b
if ok {
fmt.Println("goroutine B received data: ", val)
}
b <- data
fmt.Println("goroutine B sent data: ", data)
return
}

func main() {
ch := make(chan int)
go goroutineA(ch)
go goroutineB(ch)

time.Sleep(time.Second)
}

注意!以上代码有死锁风险!可以使用select函数解决。代码见select-case章节

单向管道

下面定义两个函数:goroutineA是一个send-only 管道, goroutineB是一个read-only 管道

在main函数中创建了一个双向通道 (ch),并且两个协程根据需要进行通道操作。

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
package main

import (
"fmt"
"sync"
)

func goroutineA(wg *sync.WaitGroup, a chan<- int) {
defer wg.Done()
data := 3
a <- data
fmt.Println("goroutine A Send data: ", data)
}

func goroutineB(wg *sync.WaitGroup, b <-chan int) {
defer wg.Done()
val, ok := <-b
if ok {
fmt.Println("goroutine B received data: ", val)
}
}

func main() {
var wg sync.WaitGroup
ch := make(chan int)

wg.Add(2)
go goroutineA(&wg, ch)
go goroutineB(&wg, ch)

// 等待goroutines完成
wg.Wait()

// 关闭通道
close(ch)
}


select-case

通过channel和select-case语句结合可以实现对多个管道的监听

1
2
3
4
5
6
select {
case v1 := <-ch1:
log.Println("recieve from ch1: ", v1)
case v2 := <-ch2:
log.Println("recieve from ch2: ", v2)
}

如果这两个管道都没有消息,就会阻塞

双向管道死锁解决:

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
package main

import (
"fmt"
"time"
)

func goroutineA(a chan int) {
data := 3
select {
case a <- data:
fmt.Println("goroutine A sent data: ", data)
case receivedData, ok := <-a:
if ok {
fmt.Println("goroutine A received data: ", receivedData)
}
}

close(a)
}

func goroutineB(b chan int) {
data := 1
select {
case val, ok := <-b:
if ok {
fmt.Println("goroutine B received data: ", val)
}
case b <- data:
fmt.Println("goroutine B sent data: ", data)
}

return
}

func main() {
ch := make(chan int)
go goroutineA(ch)
go goroutineB(ch)

time.Sleep(time.Second)
}

参考:

https://learnku.com/docs/effective-go/2020/concurrent/6249

https://zhuanlan.zhihu.com/p/60613088

https://blog.frognew.com/2015/01/go-channel-notes.html