在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等, 这会在后续内容梳理更新.