Golang 语言深入理解:channel

Python022

Golang 语言深入理解:channel,第1张

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

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

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

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

gopl/ch3/netcat3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这是违反常规的操作,理论上 goroutine 之间的 stack 是相互独立的,只有在运行时可以执行这样的操作。

这么做纯粹是出于性能优化的考虑,原来的步骤是:

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

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

queue with a lock prefered to lock-free implementation:

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

Go语言也称 Golang,兼具效率、性能、安全、健壮等特性。这套Go语言教程(Golang教程)通俗易懂,深入浅出,既适合没有基础的读者快速入门,也适合工作多年的程序员查阅知识点。

Go 语言

这套教程在讲解一些知识点时,将 Go 语言和其他多种语言进行对比,让掌握其它编程语言的读者能迅速理解 Go 语言的特性。Go语言从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验就可以轻松搞定。

Go语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具 Python 等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。

Go语言是编程语言设计的又一次尝试,是对类C语言的重大改进,它不但能让你访问底层操作系统,还提供了强大的网络编程和并发编程支持。Go语言的用途众多,可以进行网络编程、系统编程、并发编程、分布式编程。

Go语言的推出,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、语言设计良好、执行性能好”等优势,目前国内诸多 IT 公司均已采用Go语言开发项目。Go语言有时候被描述为“C 类似语言”,或者是“21 世纪的C语言”。Go 从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等很多思想,还有C语言一直所看中的编译后机器码的运行效率以及和现有操作系统的无缝适配。

因为Go语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性。Go语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说Go语言是一门混合型的语言。

此外,很多重要的开源项目都是使用Go语言开发的,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。Go 是编译型语言,Go 使用编译器来编译代码。编译器将源代码编译成二进制(或字节码)格式;在编译代码时,编译器检查错误、优化性能并输出可在不同平台上运行的二进制文件。要创建并运行 Go 程序,程序员必须执行如下步骤。

使用文本编辑器创建 Go 程序;

保存文件;编译程序;运行编译得到的可执行文件。

这不同于 Python、Ruby 和 JavaScript 等语言,它们不包含编译步骤。Go 自带了编译器,因此无须单独安装编译器。

链乔教育在线旗下学硕创新区块链技术工作站是中国教育部学校规划建设发展中心开展的“智慧学习工场2020-学硕创新工作站 ”唯一获准的“区块链技术专业”试点工作站。专业站立足为学生提供多样化成长路径,推进专业学位研究生产学研结合培养模式改革,构建应用型、复合型人才培养体系。

最近三年,在工作中使用go开发了不少服务。深感go的便捷,以及它的runtime的复杂。我觉得需要定期的进行总结,因此决定写这篇文章,也许更准确的,应该叫笔记。

最近终于解决了一个和cgo有关的问题。这个问题从发现到解决前后经历了接近4个月,当然,和人手不足也有关系。而对于我个人而言,这个问题其实历时2年!这得从头说起。

在上一家公司的一个项目里,有一个服务做音视频数据的提取,这个服务运行在嵌入式设备TX2上。音视频提取这一关键功能主要利用nvidia基于gstreamer开发的插件,这个插件可以发挥nvidia gpu的硬件解码功能。当时这个服务使用go和c混编的方式,问题的症状是服务运行一段时间后,不输出音视频数据。遗憾的是,由于疫情,项目停止,因此没有机会继续研究这个问题。

时间来到去年底。当前这个项目进行压力测试,发现关键的语音处理服务运行一段时间后,会出现不拉流的情况,因此也没有后续的结果输出。症状和上一个项目非常像。虽然使用的第三方SDK不一样,但同样用了go和c混编的方式。一开始,焦点就放在go的运行时上,觉得可能是go和c相互调用的方式不对。经过合理猜测,并用测试进行验证后,发现问题还是在第三方拉流的SDK上,它们的回调函数必须要快,否则有可能会阻塞它们的回调线程。当然,在go调用c的时候,如果耗时比较长,会对go的运行时造成一些副作用;在c回调go的时候,go的运行时也有可能阻塞c的回调线程。但go的运行时已经比较成熟,因此我觉得它对这个问题的贡献不大。以上采用了假设-验证的方法,主要的原因还是第三方的拉流SDK不开源。在定位问题的过程中,使用了gdb的gcore来生成堆栈;也搭建了灰度环境来进行压力测试,以及完善监控,这些都是解决方法的一部分。

正是这一问题,促使我更多的了解go的运行时。而我看得越多,越觉得go的运行时是一个庞大的怪物。因此,抱着能了解一点是一点的心态,不断的完善这篇笔记。