Golang 语言深入理解:channel

栏目: Go · 发布时间: 7年前

内容简介:title: Golang语言笔记: 理解 Channel 特性date: 2018-04-20 20:00:00tags: Golang

title: Golang语言笔记: 理解 Channel 特性

date: 2018-04-20 20:00:00

tags: Golang

Golang 语言深入理解:channel

image

理解 channel 特性

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

文章内容包括:

[TOC]

channel

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

element type
make
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 的诞生

Golang 语言深入理解:channel

Alt text

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

  • heap 上而不是 stack 上
  • hchan 类型
  • 返回的是指针
Golang 语言深入理解:channel

Alt text

sends and receives

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

Golang 语言深入理解:channel

Alt text

goroutine-safe 的实现

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

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

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

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

Golang 语言深入理解:channel

Alt text

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

这便是著名的 golang 并发原则的体现:

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

控制 goroutine 之间同步的实现

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

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

  • 这是怎么做到的?

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

Go runtime scheduler 负责将 goroutine 调度到操作系统线程上。

Golang 语言深入理解:channel

Alt text

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

Golang 语言深入理解:channel

Alt text

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

Golang 语言深入理解:channel

Alt text

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

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

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

Golang 语言深入理解:channel

Alt text

Golang 语言深入理解:channel

Alt text

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

接收方将通过这个 sudog 来恢复 G1

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

  1. G2 将队列中的第一个元素 task1 出队(接收 task1);
  2. 将 sendq 中的 sudog 出栈,获取到 G1 和 task4 ,然后将 task4 入队。在这里让 G2 而非 G1 执行元素入队操作的用意在于,这样 G1 恢复运行后无需再获取一次锁,将元素入队,然后再释放一次锁,即可以减少一次获取释放锁的过程;
  3. 接下来 G2 要将 G1 置为 runnable 。G2 向 runtime scheduler 发起一次 goready 调用,scheduler 将 G1 置为 runnable ,并将其入队到 runqueue,然后返回 G2。
Golang 语言深入理解:channel

Alt text

  • 当 channel 中的缓存空了,接收方继续接受,会发生什么?
Golang 语言深入理解:channel

Alt text

同样的, 接收方 G2 会被阻塞,G2 会创建 sudoq ,存放在 recvq ,基本过程和发送方阻塞一样。

不同的是,发送方 G1如何恢复接收方 G2,这是一个非常神奇的实现。

Golang 语言深入理解:channel

Alt text

理论上可以将 task 入队,然后恢复 G2, 但恢复 G2后,G2会做什么呢?

G2会将队列中的 task 复制出来,放到自己的 memory 中,基于这个思路,G1在这个时候,直接将 task 写到 G2的 stack memory 中!

Golang 语言深入理解:channel

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

Golang 语言深入理解:channel

Alt text


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

重新理解创业

重新理解创业

周航 / 中信出版集团 / 2018-10-20 / 58.00

易到用车创始人/顺为资本投资合伙人周航,首度复盘20年创业经历,全方位坦陈创业得与失。这不是一本创业成功手册,却是思想的一次出走。 20年创业经历的咀嚼与反思,从战略、品牌、竞争,到流量、领导力、团队管理等,多角度多维度的重新认知,如李开复所言,“都是真刀真枪打出来的经验,值得每一位创业者多读几遍,吸收内化”。 雷军、李开复、徐小平作序,梁建章、张志东、曾鸣推荐。一起来看看 《重新理解创业》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具