Go Channel 详解

0. 引言

channel 是 Go 语言中的一个非常重要的特性,这篇文章来深入了解一下 channel。

1. CSP

要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。简单来说,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。

2 channel 基础知识

2.1 创建 channel

channel 使用之前需要通过 make 创建。

unBufferChan := make(chan int)  // 1
bufferChan := make(chan int, N) // 2

上面的方式 1 创建的是无缓冲 channel,方式 2 创建的是缓冲 channel。如果使用 channel 之前没有 make,会出现 dead lock 错误。至于为什么是 dead lock,下文我们从源码里面看看。

func main() {
    var x chan int
    go func() {
        x <- 1
    }()
    <-x
}
$ go run channel1.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
    /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60

goroutine 4 [chan send (nil chan)]:
main.main.func1(0x0)
2.2 channel 读写操作
ch := make(chan int, 10)

// 读操作
x <- ch

// 写操作
ch <- x
2.3 channel 种类

channel 分为无缓冲 channel 和有缓冲 channel。两者的区别如下:

  • 无缓冲:发送和接收动作是同时发生的。如果没有 goroutine 读取 channel (<- channel),则发送者 (channel <-) 会一直阻塞。

    image
  • 缓冲:缓冲 channel 类似一个有容量的队列。当队列满的时候发送者会阻塞;当队列空的时候接收者会阻塞。

    image
2.4 关闭 channel

channel 可以通过 built-in 函数 close() 来关闭。

ch := make(chan int)

// 关闭
close(ch)

关于关闭 channel 有几点需要注意的是:

  • 重复关闭 channel 会导致 panic。
  • 向关闭的 channel 发送数据会 panic。
  • 从关闭的 channel 读数据不会 panic,读出 channel 中已有的数据之后再读就是 channel 类似的默认值,比如 chan int 类型的 channel 关闭之后读取到的值为 0。

对于上面的第三点,我们需要区分一下:channel 中的值是默认值还是 channel 关闭了。可以使用 ok-idiom 方式,这种方式在 map 中比较常用。

ch := make(chan int, 10)
...
close(ch)

// ok-idiom 
val, ok := <-ch
if ok == false {
    // channel closed
}

3 channel 的典型用法

1 goroutine 通信

func main() {
    x := make(chan int)
    go func() {
        x <- 1
    }()
    <-x
}
2 select

select 一定程度上可以类比于 linux 中的 IO 多路复用中的 select。后者相当于提供了对多个 IO 事件的统一管理,而 Golang 中的 select 相当于提供了对多个 channel 的统一管理。当然这只是 select 在 channel 上的一种使用方法。

select {
    case e, ok := <-ch1:
        ...
    case e, ok := <-ch2:
        ...
    default:  
}

值得注意的是 select 中的 break 只能跳到 select 这一层。select 使用的时候一般配合 for 循环使用,像下面这样,因为正常 select 里面的流程也就执行一遍。这么来看 select 中的 break 就稍显鸡肋了。所以使用 break 的时候一般配置 label 使用,label 定义在 for 循环这一层。


for {
    select {
        ...
    }
}
3 range channel

range channel 可以直接取到 channel 中的值。当我们使用 range 来操作 channel 的时候,一旦 channel 关闭,channel 内部数据读完之后循环自动结束。

func consumer(ch chan int) {
    for x := range ch {
        fmt.Println(x)
        ...
    }
}

func producer(ch chan int) {
  for _, v := range values {
      ch <- v
  }  
}
4 超时控制

在很多操作情况下都需要超时控制,利用 select 实现超时控制,下面是一个简单的示例。

select {
  case <- ch:
    // get data from ch
  case <- time.After(2 * time.Second)
    // read data from ch timeout
}

类似的,上面的 time.After 可以换成其他的任何异常控制流。

5 生产者-消费者模型

利用缓冲 channel 可以很轻松的实现生产者-消费者模型。上面的 range 示例其实就是一个简单的生产者-消费者模型实现。

4 单向 channel

单向 channel,顾名思义只能写或读的 channel。但是仔细一想,只能写的 channel,如果不读其中的值有什么用呢?其实单向 channel 主要用在函数声明中。比如。

func foo(ch chan<- int) <-chan int {...}

foo 的形参是一个只能写的 channel,那么就表示函数 foo 只会对 ch 进行写,当然你传入的参数可以是个普通 channel。foo 的返回值是一个只能读的 channel,那么表示 foo 的返回值规范用法就是只能读取。这种写法在 Golang 的原生代码库中有非常多的示例,感兴趣的可以去看一下。

// Done returns a channel which is closed if and when this pipe is closed
// with CloseWithError.
func (p *http2pipe) Done() <-chan struct{} {
    p.mu.Lock()
    defer p.mu.Unlock()
    if p.donec == nil {
        p.donec = make(chan struct{})
        if p.err != nil || p.breakErr != nil {

            p.closeDoneLocked()
        }
    }
    return p.donec
}

也许你会说这么写在功能上和使用普通的 channel 并不会有什么差别。确实是这样的。但是使用单向 channel 编程体现了一种非常优秀的编程范式:convention over configuration,中文一般叫做 约定优于配置。这种编程范式在 Ruby 中体现的尤为明显。

5 总结

Golang 的 channel 将 goroutine 隔离开,并发编程的时候可以将注意力放在 channel 上。在一定程度上,这个和消息队列的解耦功能还是挺像的。上面主要还是介绍了一些 channel 的常规操作,还有一些奇淫技巧放在参考资料里了。之后的一篇文章还是来看看 channel 的源码吧,对于更深入地理解 channel 还是挺有用的。

引自

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容

  • 作为golang并发编程思想的重要组成,channel(通道)非常重要,和goroutine(go协程)一起使用,...
    smoke_zl阅读 870评论 0 5
  • Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(c...
    空即是色即是色即是空阅读 2,339评论 0 1
  • 由浅入深剖析 go channel channel 是 golang 中最核心的 feature 之一,因此理解 ...
    不智鱼阅读 59,731评论 4 83
  • CSP 要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequenti...
    _羊羽_阅读 1,601评论 0 0
  • 协程,通道 我们在普通程序中要执行代码如下代码 错误使用协程 由于没有调度,主协程率先执行完毕,代码执行已经关闭,...
    seven_son阅读 2,100评论 0 0