Golang 语言深入理解:channel

Golang Channel Model

理解 channel 特性

本文是对 Gopher 2017 中一个非常好的 Talk�: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的学习笔记,希望能够通过对 channel 的关键特性的理解,进一步掌握其用法细节以及 Golang 语言设计哲学的管窥蠡测。

channel 的概念

channel 是可以让一个 goroutine 发送特定值到另一个 gouroutine 的通信机制。

  • 可以理解为某种类型的值传递的导管,而这种在 channel 中传递的类型成为 channle 的 element type 元素类型。
  • 一个使用make创建的,对数据结构的引用,当把 channel 作为参数使用时,实际上是传引用调用
  • channel 的零值: nil

如何使用 channel?

原生的 channel 是没有缓存的(unbuffered channel),可以用于 goroutine 之间实现同步。

  • 发送 sends 和接收 receives
ch := make(chan int) // ch hase type `chan int`

ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded
  • 关闭 close
close(ch)

关闭后不能再写入,可以读取直到 channel 中再没有数据,并返回元素类型的零值。

  • buffered channel的创建
ch := make(chan int) // unbuffered channel
ch := make(chan int, 0) // unbuffered channel
ch := make(chan int, 3) // buffered channel with capacity 3

buffered channel 可以用于非常方便的实现生产者-消费者模型,实现异步操作。

使用 unbuffered channel 实现同步

gopl/ch3/netcat3

func main() {
    conn, err := net.Dial("tcp", "localhost:8008")
    if err != nil {
        log.Fatal(err)
    }
    // communication over an buffered channel causes the sending and
    // receiving goroutines to synchronize
    done := make(chan struct{})
    go func() {
        io.Copy(os.Stdout, conn) // NOTE; ignoring errors
        log.Println("Done")
        done <- struct{}{} // signal the main goroutine
    }()
    mustCopy(conn, os.Stdin)
    conn.Close()
    <-done
}

channel 的特性

  • goroutine-safe
    goroutine 安全
  • store and pass values between goroutines
    存储数据并在 goroutine 之间传递数据
  • provide FIFO semantics
    提供 FIFO 语义
  • can cause goroutines to block and unblock
    可以使得 goroutine 阻塞或者释放

以上特性是怎么实现的?

making channels

首先从 channel 是怎么被创建的开始:

一个 channel 的诞生

creating channel

heap上分配一个hchan类型的对象,并将其初始化,然后返回一个指向这个hchan对象的指针。

  • heap上而不是stack
  • hchan类型
  • 返回的是指针
Alt text

sends and receives

理解了 channel 的数据结构实现,现在转到 channel 的两个最基本方法: sendsreceivces ,看一下以上的特性是如何体现在 sendsreceives 中的:

Alt text

goroutine-safe 的实现

假设发送方先启动,执行ch <- task0:

  1. 获取lock,加锁;
  2. Task类型的对象task0执行入队操作;
  3. 完成入队操作后,释放锁

需要特别指出的是,这里的入队enqueue操作实际上是一次memcopy行为,将整个task0复制一份到buf,也就是FIFO缓冲队列中。

如此为 channel 带来了 goroutine-safe的特性。

Alt text

在这样的模型里,sender goroutine -> channel -> receiver goroutine 之间,hchan是唯一的共享内存,而这个唯一的共享内存又通过mutex来确保goroutine-safe,所有在队列中的内容都只是副本。
这便是著名的 golang 并发原则的体现:

不要通过共享内存来通信,而是通过通信来共享内存。

控制 goroutine 之间同步的实现

  • 当 channel 中的缓存满了,发送方继续发送,会发生什么?

发送方 goroutine 会阻塞,暂停,并在收到receive后才恢复。

  • 这是怎么做到的?

goroutine 是一种用户态线程, 由 Go runtime 创建并管理,而不是操作系统,比起操作系统线程来说,goroutine更加轻量。
Go runtime scheduler 负责将 goroutine 调度到操作系统线程上。

Alt text

runtime scheduler 怎么将 goroutine 调度到操作系统线程上?

Alt text

当阻塞发生时,一次 goroutine 上下文切换的全过程:

Alt text
  1. 当发送方 goroutine 向已经满了的 channel 发送数据后,发生了 goroutine 的阻塞,goroutine 会对 runtime scheduler 发起一次gopark调用;
  2. 当 sheduler 接收到gopark调用,会将现在正在运行的 goroutine G1running置为waiting;
  3. 并将G1和承载它的操作系统线程M之间的联系解除;
  4. 从 runqueue 调度一个新的runnable goroutine G,并将其和M绑定,开始执行G

只是阻塞了 goroutine,没有阻塞操作系统线程。

然而,被阻塞的 goroutine 怎么恢复过来?

Alt text
Alt text

阻塞发生时,调用 runtime sheduler 执行gopark之前,G1 会创建一个sudog,并将它存放在hchansendq中。sudog中便记录了即将被阻塞的 goroutine G1,以及它要发送的数据元素task4等等。
接收方将通过这个sudog来恢复 G1

接收方 G2 接收数据, 并发出一个receivce,将 G1 置为 runnable:

  1. G2 将队列中的第一个元素 task1 出队(接收 task1);
  2. sendq中的sudog出栈,获取到G1task4,然后将task4入队。在这里让 G2 而非 G1 执行元素入队操作的用意在于,这样 G1 恢复运行后无需再获取一次锁,将元素入队,然后再释放一次锁,即可以减少一次获取释放锁的过程;
  3. 接下来 G2 要将 G1 置为 runnable。G2 向 runtime scheduler 发起一次goready调用,scheduler 将 G1 置为runnable,并将其入队到 runqueue,然后返回 G2。
Alt text
  • 当 channel 中的缓存空了,接收方继续接受,会发生什么?
Alt text

同样的, 接收方 G2 会被阻塞,G2 会创建sudoq,存放在recvq,基本过程和发送方阻塞一样。
不同的是,发送方 G1如何恢复接收方 G2,这是一个非常神奇的实现。

Alt text

理论上可以将 task 入队,然后恢复 G2, 但恢复 G2后,G2会做什么呢?
G2会将队列中的 task 复制出来,放到自己的 memory 中,基于这个思路,G1在这个时候,直接将 task 写到 G2的 stack memory 中!

Alt text

这是违反常规的操作,理论上 goroutine 之间的 stack 是相互独立的,只有在运行时可以执行这样的操作。
这么做纯粹是出于性能优化的考虑,原来的步骤是:

  1. G1 获取锁,将 task 进行入队(memcopy)
  2. 当 G2 恢复时,获取锁并且读取 task(memcoy)

优化后,相当于减少了 G2 获取锁并且执行 memcopy 的性能消耗。

总结: 特性的实现

  • goroutine-safe:
    • 通过 hchan mutex 实现
  • store values, pass in FIFO:
    • 通过 hchan buffer实现
    • 通过共享 hchan buffer 进行传值(实际上是传递副本)
    • 唯一共享的 buffer 使用 hchan mutex 保证 goroutine 安全
  • can cause goroutines to pause and resume
    • 通过 hchan sudog queues实现
    • 调用 runtime scheduler(gopark,goready)

simplicity 和 performance 的权衡

channel 设计背后的思想可以理解为 simplicity 和 performance 之间权衡抉择,具体如下:

Simplicity

queue with a lock prefered to lock-free implementation:

比起完全 lock-free 的实现,使用锁的队列实现更简单,容易实现

The performance improvement does not materiablize from the air, it comees with code complexity increase.
性能的提升(lock-free)不是凭空实现的,它来自代码复杂性的增长。

Performance

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

推荐阅读更多精彩内容