五分钟搞定Channel|Go主题月

786 阅读4分钟

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是接收处理到的位置。

sendqrecvq 就是前面提到的先进先出的队列,分贝存储了等待发送和接收数据的goroutine,具体的结构体定义是waitq,其中的sudog就是goroutine,每个列表都是双向的。

type waitq struct {
	first *sudog
	last  *sudog
}

整体结构如图所示,图片来自《图解channel的底层实现》

image.png

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)
}

参考资料

A Tour Of Go

Concurrency in Go

Go 语言设计与实现

图解channel的底层实现

Goroutine与Channel详解

曹春晖大佬的文章