0%

Go Concurrency programming Channel

Gogoroutine的同步机制主要包括channelsync. 本文介绍channel的简单定义与使用.

缘由

Go中可以轻松的发起数千甚至上万级别的goroutines, 但是仅仅发起goroutine而不做同步限制是无意义的, 需要goroutine之间可以通信, 此时Go提出了概念:channel, 中文通常称为通道.

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信, 其推动诞生了channel; 对于传统的通过共享内存进行通信过程中, 不同的线程访问同一内存区域获取资源, 为了防止race condition就需要通过mutex方式处理, 这势必降低了性能表现; 对于通过通信共享内存的方式(channel), 在相关goroutine之间创建了隧道, 通信不被其他goroutine感知, 自然没有race condition问题, 提升了性能表现.

用处

一句话说: channel是用于goroutine间通信的机制. 因其特点, 具备多种用处:

  • 同步控制: channel可控制同步执行goroutine, 通过send/receive value with channel可以控制特定goroutine的生命周期, 这可以防止race condition, 保证程序运行的安全性;
  • 数据通信: channel可以用于goroutines间的数据通信, 这使得复杂的工作拆解为多个简单的工作;
  • 并发: channel的出现, 也使得将复杂工作拆分为多个简单工作(goroutine)存在可能性, 这也就进一步利用了concurrency的特性;
  • 错误处理:channel也可以用于Error Handing错误处理, 通过Error Channel可以将错误信息传递给其他并发环境, 进而进行优雅处理;
  • 资源分享:channel可以用于goroutines间的资源共享, 例如传递refchannel;

基本使用

声明

channel是引用类型, 空值为nil, 声明格式如下:

1
var Variable chan Type

例如:

1
2
3
var ch1 chan int // 声明一个传递数字的通道, 为nil
var ch2 chan bool // 声明一个传递布尔值的通道, 为nil
var ch3 chan []int // 声明一个传递int切片的通道, 为nil

创建

通过make关键字+chan关键字可以创建channel, 如:

1
make(chan Type, [buffer size])
  • Type: 指的是channel中的元素类型;
  • buffer size: 指的是channel缓冲区大小, 不填则表示为无缓冲channel.

例如:

1
2
3
ch1 := make(chan int) // 创建一个无缓冲的int通道
ch2 := make(chan int, 5) // 创建一个缓冲大小为5的int通道
ch3 := make(chan bool) // 创建一个无缓冲的bool通道

发送&接收

通过标识符<-可以实现发送值到通道, 从通道接收值的操作:

  • 发送值, ch <- value:

    1
    ch <- 15 // 发送15到channel ch中
  • 接收值, varable := <- ch:

    1
    a := <- ch // 接收一个channel value, 并初始化为变量a

    关闭

通过close关键字, 可以关闭一个通道.示例:

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() {
ch := make(chan int)

go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // Close the channel after sending all values
}()

for value := range ch {
fmt.Println(value)
}
}

上面的例子同, 起了一个goroutine实现发送值操作, 并在发送完毕后关闭了通道;

使用场景

无缓冲通道

channel具备有两个重要的属性: len, cap. len(ch)可以获得ch中当前的数据量, cap(ch)可以获得ch中的容量, 就是在初始化时make(chan Type, [buffer size])中的buffer size. 很明显len <= cap.

无缓冲通道就是buffer size == 0的通道. 无缓冲通道根据其特点也成为阻塞通道, 同步通道;

  • 在发送值时: 发送值到Unbuffered Channelsender将会阻塞, 直到valuereceiver接收;
  • 在接收值时: receiver将会阻塞, 直到从Unbuffered Channel接收到value;

就像是快递员送快递,必须送到客户的手上, 中间没有快递柜. 客户在获得快递前, 就在楼下傻呆着等快递员送过来.示例:

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

func main() {
ch := make(chan int)
// Receiver
go func() {
fmt.Println("Start receiving ....")
// Do Something
value := <-ch
fmt.Printf("Receive %v \n", value)
// Do Something
fmt.Println("End receiving ....")
}()
// Sender
go func() {
fmt.Println("Start sending ....")
// Do Something
fmt.Println("Send 1")
ch <- 1
// Do Something
fmt.Println("End sending ....")
close(ch)
}()

time.Sleep(2 * time.Second)
}

上面的例子中创建了一个无缓冲通道ch, 并通过匿名函数的方式, 发起了两个协程, 一个作用为receiver, 一个作用为sender. 两个匿名函数中均通过关键日志的方式输出了操作内容, 可以发现不管如何执行, 接收操作一定是在发送操作之后发生的, 也就是说: Receive %v \n的日志必定出现在Send 1日志之后, 其他日志由于并发导致执行顺序不确定. 这就是同步通道/阻塞通道的由来;

有缓冲通道

理解了无缓冲通道, 有缓冲通道就是指的时Buffer Size不为0的通道, 就像是在快递员与顾客之间添加了快递柜, Buffer Size就是快递柜的个数, 示例代码:

1
2
3
4
5
func main() {
ch := make(chan int, 1) // Init Buffered Channel with Size 1
ch <- 15
fmt.Println("End Sending .... ")
}

有缓冲通道与无缓冲通道, 并没有本质区别. 例如上面提到的阻塞通道的用处, 当Sender向一个len==cap的通道发送时, 也会被阻塞. 当Receiver像一个len == 0的通道接收时, 也会被阻塞;

同步与阻塞

同步与阻塞在实际时往往会借助于无缓冲通道实现, 可以实现goroutines之间的同步机制, 而不必借助于sync/mutex包;

优雅通道取值

通道取值的基础方式为: varable := <- ch, 这种方式存在一个缺陷, 当通道已经没有数据时, 其会返回对应数据类型的零值, 这会困扰Receiver,无法得知这是否是Sender所发.

也可以通过: if variable, ok := <- ch; ok { ...}进行判定, 当通道存在值时ok == true; 但是这并不优雅, 一般推荐使用for range语句获取, 其内置会判定ch是否已经关闭了, 关闭后不再取值;示例:

1
2
3
for value := range ch {
// Process the received value
}

单向通道

单向通道用于限定函数中通道参数的用途, 仅发送, 仅接收.

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
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
  1. chan<- int是一个只能发送的通道,可以发送但是不能接收;
  2. <-chan int是一个只能接收的通道,可以接收但是不能发送。

结论

本文介绍了Channel的来源, 基本用处, 基本使用, 基本场景等内容, 通过本文可以对Channel有一个初步的了解. 在实际项目中, 其应用会更加丰富,例如结合Select, Context等, 这会在后续内容梳理更新.