Go语言的一个设计思想就是基于通信顺序进程(Communicating sequential processes, CSP),该思想来自1978年Hoare的论文。在文章中,CSP也是作者自定义的编程语言,定义了输入输出语句,用于 processes 间的通信,processes 被认为是需要输入驱动,并且产生输出,供其他 processes 消费,processes 可以是进程、线程、甚至是代码块。通过这些输入输出命令,Hoare 证明了如果一门编程语言中把 processes 间的通信看得第一等重要,那么并发编程的问题就会变得简单。Go语言将这一思想发扬广大,而channel就是实现Goroutine间传递信息的基础。
Channel用法
声明
和map、slice一样,channel可以使用make
关键字来进行初始化。channel具有类型属性,一个channel只能接受对应类型的输入输出。
ch := make(chan int) //不带缓冲的channel
ch := make(chan int,2) //带缓冲的channel
类似于map、slice可以有个初始大小,channel定义时可以选择是否有缓冲,无缓冲的channel,当一个goroutine向里面写入时会一直被阻塞,直到另外一个goroutine从里面取走值;而有n个缓冲的channel允许先输入n个值,只有当缓冲满了,写入第n+1个值时才会阻塞。
读写数据
我们通过<-
操作符对channel
来进行读写操作,箭头的方向就定义了数据流动的方向。
ch := make(chan int)
ch <- 2 // 将 2 发送至channel中
v := <-ch // 从 ch 接收值并赋予 v
同样的,我们也可以在声明/初始化channel的时候,定义只读/只写的channel,但只读/只写的channel没有太大意义,一般只在函数参数中这样声明。
var readCh <-chan int // 只读
var writeCh chan<- int // 只写
var ch chan int //读写
或者类似下面
readCh := make (<-chan int,10) //只读
writeCh := make (chan<- int,10) //只写
ch := make (chan int,10) //读写
另外,还可以使用range来不断从一个channel中读取数据
c := make(chan int, 10)
go fibonacci(10, c)
for i := range c {
fmt.Println(i)
}
关闭channel
一个channel被声明后,可以一直被用来读写数据,大部分情况下我们不需要主动清理channel,当需要时,可以用close
来主动关闭一个channel,并且只有发送者才能关闭信道,而接收者不能。如果向一个已经关闭的信道发送数据会引发程序panic。
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
select关键字
Go里面的select类似于针对channel的switch语句,里面的每个case都是通信操作,如果任意一个case可以运行,就会执行对应的语句;如果有多个 case 都可以运行,会随机选择一个case执行;如果都不满足,那么就会一直阻塞,直到有满足条件的,也正因此,实际使用中每个select最好都有一个
package main
import (
"fmt"
"time"
)
func goRoutineA(ch chan int, i int) {
for {
time.Sleep(time.Second * 2)
ch <- i
}
}
func goRoutineB(ch chan string, in string) {
for {
time.Sleep(time.Second * 3)
ch <- in
}
}
func main() {
intCh := make(chan int, 5)
stringCh := make(chan string, 5)
go goRoutineA(intCh, 5)
go goRoutineB(stringCh, "ok")
i := 1
ok := true
for ok {
select {
case msg := <-intCh:
fmt.Println(i, " A input data ", msg)
case msg := <-stringCh:
fmt.Println(i, " B input data ", msg)
default:
fmt.Println(i, "no data ")
time.Sleep(time.Second * 1)
}
i++
if i > 60 {
ok = false
}
}
}
Channel底层实现
channel是解决不同goroutine之间通信的问题,那么对于channel就很可能面临多个写入的goroutine和多个读取的goroutine的情况,channel的底层就采用了先进先出的队列
来维护这一情况,具体结构定义在src/runtime/chan.go
,如下
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
channel是有lock
属性的,用于处理并发的情况,此外elemtype
是Channel 能够收发的元素类型,elemsize
是元素大小,closed
属性标识channel是否已经被关闭。
和处理缓冲区数据有关的属性主要有五个:qcount
是缓冲区的数据个数,dataqsiz
是缓存区的大小,buf
是指向缓冲区的指针,sendx
是当前发送处理到的位置,recvx
是接收处理到的位置。
sendq
和 recvq
就是前面提到的先进先出的队列,分贝存储了等待发送和接收数据的goroutine,具体的结构体定义是waitq
,其中的sudog
就是goroutine,每个列表都是双向的。
type waitq struct {
first *sudog
last *sudog
}
整体结构如图所示,图片来自《图解channel的底层实现》
Channel的其他问题
channel是如何通知对应goroutine进行操作的
这涉及到了Go语言最底层的 GMP
模型,前面提到每个goroutine队列的子节点是sudog
,而sudog
中最底层存的是g
,当channel收发数据导致相应gorouinte状态发生变化时,实际是调用更底层方法将g
的状态进行了修改,并没有真正让goruotine立刻运行。
goroutine之间通信除了channel,还有其他方式吗
当然有,最常见的是共享内存,比如定义一个全局变量,多个goroutine共享。还有一种方式是context
,直译为上下文,也是go里面比较有意思的一种机制。
为什么建议在读写channel时使用匿名函数
并不是建议用匿名函数,而是要另起一个goroutine,避免死锁。 比如下面这个例子,创建了channel,向里面写数据,由于没有取数据,就会一直阻塞,时间长了系统就判定死锁了。
func main() {
ch := make(chan string)
ch <- "send"
}
而如果改成直接另一个goroutine,就不会有这个问题,因为主goroutine该干啥干啥,被阻塞的是子goroutine,最终程序可以正常退出。
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
time.Sleep(time.Second * 3)
}