Channels In Go
channel 是一个Go重要的内建feature。和goroutine是两个独特的features让Go的并发特点是十分的便捷和有趣的。并且能极大的降低并发编程的难度。
这篇文章将会罗列一些channel相关的概念,语法和规则。为了更好的理解channel,会简单解释一下channel的内部结构和一些Go编译器或运行时的实现细节。
这些信息对于新的gophers来说可能比较专业化,文章的一些部分可能需要多读几遍才能更好理解。
Concurrent Programming And Concurrency Synchronization
现代CPU一般都是多核,一些CPU甚至支持超线程。换种说法,现在CPU能够同时处理多条指令通道。为了能充分利用CPU性能我们需要在项目中运用并发编程。
并发计算是一种计算形式,就是在同一时间段执行若干计算。下图是两种并发计算的cases。在图中,A和B代表两个独立的运算。第二个案例也叫平行计算,是一个种特殊的并发计算。在第一个案例种,A和B只是在某个小的时间片中平行而已。
并发计算将会共享资源,一般是内存资源。
如下是一些并发计算会发生的场景:
- 某个时间一个程序在写入某个内存段,与此同时有另一个程序在读取该内存段,那么可能读取到未保存的脏数据
- 某个时间一个程序在写入某个内存段,于此同时有另一个程序也在写数据到该内存段,那么可能会覆盖未保存的数据
这些场景都叫做data races。并发编程的一个职责就是去控制资源的共享问题,让竞争不会发生。执行这个职责的方式就叫做concurrency synchroniazaiton或者data synchoronizaiton。Go语言支持多种数据同步,接下来介绍其中的一种,叫做channel。
当然其他的并发职责包括:
- 确定需要多少并发程序
- 确定一个程序起始终止的时间
- 确定每一个并发程序的工作内容
大多数的操作在Go中不是同步的,也就是说他们不是线程安全的。这些操作包括声明变量,参数传递和集合等容器对象的操作等等。只有一些操作是线程安全的,比如channel。
在Go中,我们用goroutine代表每一个程序计算单位。
Channel Introduction
Rob Pike曾在并发编程中的提出一个建议:
don't (let goroutines) communicate by sharing memory, (let them) share memory by communicating (through channels).
channel的设计就是源于这条哲理。
communicating by sharing memory 和 sharing memory by communicating 是两种并发编程的两种模式。前者我们需要传统的并发控制手段,比如mutex锁来保护共享内存,避免数据竞争。
Go提供channel来实现后者的方式。我们可以把channel当作一个内部的FIFO队列。一些协程向channel发送数据,一些协程从这个channel来接受数据。
那些通过channel传递的值,它的所有权也会通过channel传递。我们可以认为传递的值的所有权也跟着传递到接受的协程中。The values (whose owerships are transferred) are often referenced (but are not required to be referenced) by the transferred value.
需要明确一点,上述所说的并不是绝对的所有权转移。
我们所说的所有权是逻辑层面的所有权,不是像Rust语言那样,Go并不保证语法层的所有权。Go能帮助程序员写出无数据竞争的代码,但是不能保证程序本身是错的。
chanenl是基础语法不是sync包之类中的,不需要导包。
Channel Types And Values
Channel是一个复合类型。就像array,slice和map,每一个类型都有对象类型。每一个输入channel的类型必须是所指定的对象类型。
Channel类型可以是无指向性或者单指向性的。我们假设T为随意的数据类型。
- chan T 表示无指向性类型。编译器允许接收或者发送数据。
- chan<- T 表示send-only类型。编译器不允许从该channel接收数据。
- <-chan T 表示receive-only类型。编译器不允许向该channel发送数据。
T 表示channel的对象类型
chan T 可以变成 chan<- T和 <-chan T ,反之亦然。同样单向性的channel也不能互转。
channel有容量属性,如果0容量表示unbuffered channel,反之叫做buffered channel。
channel的零值表示为 nil 。一个非nil的channel值必须用内建函数 make 来创建,该函数第二个值表示容量,是可选的,默认为0。
Channel Assignments And Comparisons
All channel types are comparable types.
From the article value parts, we know that non-nil channel values are multi-part values. After one channel value is assigned to another, the two channels share the same undrelying part(s). In other words, the two channels represent the same internal channel object. The result of comparing them is true
.
Channel Operations
对于channel来说有五个操作。我们假设用ch来表示一个channel变量。
- 用如下的内建函数关闭一个channel
close(ch)
// The close built-in function closes a channel, which must be either // bidirectional or send-only. It should be executed only by the sender, // never the receiver, and has the effect of shutting down the channel after // the last sent value is received. After the last value has been received // from a closed channel c, any receive from c will succeed without // blocking, returning the zero value for the channel element. The form // x, ok := <-c // will also set ok to false for a closed channel. func close(c chan<- Type)
close只能是发送方关闭。
- 向channel发送数据
ch <- V
值得注意的是V必须是channel的制定的数据类型 - 从channel接收数据
<- ch
channel的接收操作肯定至少返回一个结果。
channel返回值有两个,第二个可选参数表示该值是否是在channel关闭的之前接收的。
v = <- ch
v, sentBeforeClosed = <- ch - 查询buffer的容量
cap(ch) - 查询当前已经使用的容量,表示当前channel中还未被接收返回的数据
len(ch)
这些操作都是确保同步的,不需要更多的操作去控制他们的同步。但是,其他的就不保证了。
A Simple Summary Of Channel Operation Rules
为了更加简单清晰的解释channel,接下来的文章里,将channel分为如下几个部分
- nil channels
- non-nil but closed channels
- not-closed non-nil channels