在Go
中goroutine
的同步机制主要包括channel
与sync
. 本文介绍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
间的资源共享, 例如传递ref
到channel
;
基本使用
声明
channel
是引用类型, 空值为nil
, 声明格式如下:
1 | var Variable chan Type |
例如:
1 | var ch1 chan int // 声明一个传递数字的通道, 为nil |
创建
通过make
关键字+chan
关键字可以创建channel
, 如:
1 | make(chan Type, [buffer size]) |
- Type: 指的是
channel
中的元素类型; - buffer size: 指的是
channel
缓冲区大小, 不填则表示为无缓冲channel
.
例如:
1 | ch1 := make(chan int) // 创建一个无缓冲的int通道 |
发送&接收
通过标识符<-
可以实现发送值到通道, 从通道接收值的操作:
发送值,
ch <- value
:1
ch <- 15 // 发送15到channel ch中
接收值,
varable := <- ch
:1
a := <- ch // 接收一个channel value, 并初始化为变量a
关闭
通过close
关键字, 可以关闭一个通道.示例:
1 | package main |
上面的例子同, 起了一个goroutine
实现发送值操作, 并在发送完毕后关闭了通道;
使用场景
无缓冲通道
channel
具备有两个重要的属性: len, cap
. len(ch)
可以获得ch
中当前的数据量, cap(ch)
可以获得ch
中的容量, 就是在初始化时make(chan Type, [buffer size])
中的buffer size
. 很明显len <= cap
.
无缓冲通道就是buffer size == 0
的通道. 无缓冲通道根据其特点也成为阻塞通道, 同步通道;
- 在发送值时: 发送值到
Unbuffered Channel
后sender
将会阻塞, 直到value
被receiver
接收; - 在接收值时:
receiver
将会阻塞, 直到从Unbuffered Channel
接收到value
;
就像是快递员送快递,必须送到客户的手上, 中间没有快递柜. 客户在获得快递前, 就在楼下傻呆着等快递员送过来.示例:
1 | package main |
上面的例子中创建了一个无缓冲通道ch
, 并通过匿名函数的方式, 发起了两个协程, 一个作用为receiver
, 一个作用为sender
. 两个匿名函数中均通过关键日志的方式输出了操作内容, 可以发现不管如何执行, 接收操作一定是在发送操作之后发生的, 也就是说: Receive %v \n
的日志必定出现在Send 1
日志之后, 其他日志由于并发导致执行顺序不确定. 这就是同步通道/阻塞通道的由来;
有缓冲通道
理解了无缓冲通道, 有缓冲通道就是指的时Buffer Size
不为0
的通道, 就像是在快递员与顾客之间添加了快递柜, Buffer Size
就是快递柜的个数, 示例代码:
1 | func main() { |
有缓冲通道与无缓冲通道, 并没有本质区别. 例如上面提到的阻塞通道的用处, 当Sender
向一个len==cap
的通道发送时, 也会被阻塞. 当Receiver
像一个len == 0
的通道接收时, 也会被阻塞;
同步与阻塞
同步与阻塞在实际时往往会借助于无缓冲通道实现, 可以实现goroutines
之间的同步机制, 而不必借助于sync/mutex
包;
优雅通道取值
通道取值的基础方式为: varable := <- ch
, 这种方式存在一个缺陷, 当通道已经没有数据时, 其会返回对应数据类型的零值, 这会困扰Receiver
,无法得知这是否是Sender
所发.
也可以通过: if variable, ok := <- ch; ok { ...}
进行判定, 当通道存在值时ok == true
; 但是这并不优雅, 一般推荐使用for range
语句获取, 其内置会判定ch
是否已经关闭了, 关闭后不再取值;示例:
1 | for value := range ch { |
单向通道
单向通道用于限定函数中通道参数的用途, 仅发送, 仅接收.
1 | func counter(out chan<- int) { |
chan<- int
是一个只能发送的通道,可以发送但是不能接收;<-chan int
是一个只能接收的通道,可以接收但是不能发送。
结论
本文介绍了Channel
的来源, 基本用处, 基本使用, 基本场景等内容, 通过本文可以对Channel
有一个初步的了解. 在实际项目中, 其应用会更加丰富,例如结合Select, Context
等, 这会在后续内容梳理更新.