专栏首页码农桃花源深度阅读之《Concurrency in Go》

深度阅读之《Concurrency in Go》

很多朋友可能都知道,年前我从滴滴跑路,来了字节跳动,并且很快就写了篇传播甚广的弱智找 bug 文章:《“���”引发的线上事故》,之后就再也没动静了……

当然最主要的原因还是这边工作的节奏比较快,而且还是大小周,工作之外,基本没有写作的精力了。当然,根本的原因还是水平太低,因为像左神就高产似那啥,所以还是得多从自己身上找找原因。

虽然工作比较累,但不得不说字节工作环境还是不错的,除了免费的三餐、听不完的分享、各路大神之外,还有非常好用的飞书……所以,这里很自然地就出现了内推广告:

校招:

字节跳动校招内推码: U6FPT3Y https://job.toutiao.com/s/JkKrnq4

社招:

https://job.toutiao.com/s/JkKRuj4

这里还有一个直达我们团队的校招后端职位(公众号后台回复“校招”获得更多职位),也欢迎直接投这个:

字节跳动校招内推码: U6FPT3Y https://job.toutiao.com/s/JkKMayq

之前在朋友圈也推过一次,投的同学还是挺多的。除了技术,其他如运营、产品也可以通过这个渠道投递。好处是可以加我微信(raoquancheng1991),我帮你查看&&催进度,必要的时候我可以帮你完善下简历。欢迎大家投递,并且分享给需要的朋友,在此先谢过了~顺便说一句,我看不到你投递的简历内容,只能看到一条投递到某个岗位的记录。

说回正事,回到文章以及《Concurrency in Go》这本书。作为一个终身学习者,输入和输出是必不可少的。输入多了之后,会发现很多中文文章很难读,可能还有很多错漏之处。不客气地说,输入的是垃圾,输出的只能是垃圾。

曹大经常说需要多看英文资料[1],包括各种新出的英文书、文章等等,这从他的书单[2]也可以看出来。我自己的情况是:英文资料读的不多,英文技术书则基本就没完整地读过一本。之前在写文章的过程中,还是看了一些英文文章,收获很大。

这次尝试读一读英文技术书。但是直接读的话,经常读完和没读一样,没有什么感觉。于是我尝试一种边读书边记读书笔记的方式,过程中读到有趣的、有用的、以前不知道的地方就记下来,和大家分享。

这是一本 2017 年 7 月份出版的书,到今天已经过去三年了,Go 的版本也从当时的 Go 1.8,升级到了最新的 Go 1.15,变化巨大。

下面是我记的笔记:

  1. 并发程序经常出错的一个原因是人们认为自己所写代码的执行顺序是按书写的顺序来执行的,但在并发场景下,这显然是有问题的。
  2. Atomicity,原子性。谈论原子性,必须要有一个 context。因为在一个 context 下是原子性的,但在另一个 context 下,就可能不是原子性的了。具体的 context 可能是:进程、操作系统、机器、集群……假想个例子,在一维空间中的 X 轴上,从坐标 1 到坐标 3 必须要经过坐标 2,这在一维空间中是绝对正确的。但作为活在三维空间里的人,我有很多种办法不经过 X 轴上的坐标 2 而到达坐标 3。仅管我的轨迹映射到 X 轴上还是会“经过”坐标 2,这也更像一个“降维打击”的例子。
  3. 形成死锁的四个条件:Mutual Exclusion(并发实体任意时刻独占资源)、Wait For Condition(并发实体同时持有资源并都在等待其他资源)、No Preemption(资源只能被持有它的实体释放)、Circular Wait(循环等待,a 等 b,b 等 c,c 等 a……)。
  4. 活锁是饥饿的一种,任何需要分享的资源都有可能发生饥饿,如 CPU、内存、文件句柄、数据库连接等。
  5. 并发(Concurrency)说的是代码,并行(Parallelism)说的是正在运行的程序。我们无法写出并行的代码,只能写并发的代码,并且期望它能并行执行。想象一下,我们写的代码在单核 CPU 上运行,还能并行地起来吗?
  6. 考察并发的代码是否是在并行执行,我们得看在哪一个抽象的层级上看:并发原语、程序的运行时、操作系统、操作系统所在的平台(容器、虚拟机……)、CPUs、机器、集群……
  7. 和前面说的 Atomicity 一样,谈论 Parallelism 时,也要有一个 context。它决定是否将能将两个操作看成并行。例如,我们运行 2 个操作,每个操作花费 1 秒。如果 context 是 5 秒钟,那可以说这两个操作是在并行执行;但如果 context 是 1 秒钟,那我们认为,这两个操作是串行地在执行。注意,context 并不等同于时间,线程、进程、操作系统等都可以看成 context。
  8. 给并发或者说并行定义什么样的 context 和并发程序是否正确运行有很大关系。例如,context 是两台电脑,我们分别在两台电脑上运行两个计算器程序,那理论上这两个计算器程序就是并行的,且不会相互影响。
  9. 在上面的例子里,context 是两台电脑,operations 是两个进程。很明显,我在我的电脑上运行任何程序,都不会影响你的电脑。但是在同一台机器上,一个进程还能保证不影响另一个进程吗?回答是不一定,比如读写同一个文件……
  10. 大部分程序的并发抽象层级是线程。Go 在抽象层级上又增加了一个 goroutine。按理说,层级层次越高,并发安全性越难保证。但实际上 goroutine 让事情变得更容易,因为它并不是在线程的抽象层级之上又加了一层,而是取代了线程。
  11. Go channel 的设计思想来源于 Hoare 于 1978 年发表在 ACM 上的一篇关于 CSP(Communicating Sequential Processes)的论文。Go 是第一门吸收了 CSP 精华并且将其发扬光大的语言。
  12. 大多数语言使用线程+并发同步访问控制作为并发模型,而 Go 的并发模型由 goroutine 和 channel 组成。线程类似于 goroutine,而并发同步访问控制则类似于 mutex。
  13. Go 并发的理念是:简单,尽量使用 channel,尽情使用 goroutine。
  14. 在 linux 上,简单测试线程切换成本:
# 在 CPU0 上执行,在两个内核线程间发送、接收消息
taskset -c 0 perf bench sched pipe -T

因为是单核,所以在两个线程间发送、接收消息,需要进行上下文切换。在我的乞丐版阿里云主机上得到结果:

# Running 'sched/pipe' benchmark:
# Executed 1000000 pipe operations between two threads

     Total time: 69.171 [sec]

      69.171280 usecs/op
          14456 ops/sec

计算出大致的线程切换成本:69.171280/2 = 34.58564 us。

  1. 使用 sync.WaitGroup 时要注意,sync.Add 要在新起 goroutine 语句的外层调用,否则执行到 sync.Wait 时,可能新起的 goroutine 还没调度到,sync.Add 自然没执行,最终导致逻辑出错。
  2. mutex 是 mutual exclusion 的简写,翻译一下:互相排斥。
  3. sync.cond 有两个比较有意思的方法:sync.Cond.Signal 和 sync.Cond.Broadcast。前者会唤醒等待时间最长的 goroutine,后者会唤醒所有等待的 goroutine。另外,要注意 sync.Cond.Wait 方法内部,隐藏了一些副作用,会先解锁:c.L.Unlock(),然后再加锁:c.L.Lock()
  4. 查询 Go 源码使用了多少次 sync.Once:
grep -ir sync.Once $(go env GOROOT)/src | wc -l
  1. channel 是粘合 goroutine 的胶水,select 则是粘合 channel 的胶水。
  2. 关于 runtime.GOMAXPROCS(n) 函数的一个可能的使用场景:代码中可能存在 data race 的情况,增加 n 值可以让 data race 更快地发生,从而可以更快地调试错误。
  3. 为了避免 goroutine 泄露,请注意:生成子 goroutine 的父 goroutine 需要负责停止子 gotoutine,即谁创建谁销毁。
  4. 可以将一个“无序、耗时长”的 stage 转成 fan-out。fan-in 是多转一,fan-out 则是一转多。
  5. 设计系统的时候,应该一开始就考虑 timeout 和 cancel。
  6. 分布式系统需要支持 timeout 的几个理由:
  • 饱和 系统饱和时,最后到达的请求需要直接超时返回,否则可能引发雪崩;
  • 数据过期 数据其实有一定的时间窗口,过了窗口,就是无效数据了。例如前端一个请求过来,假设用户可以容忍 2s,那这个窗口就是 2s,分布式系统需要支持 2s 的超时设置,超过 2s 后数据无效;
  • 防止死锁 当然,触发 timeout,有可能使死锁变成活锁。系统设计的目标应该是在不触发 timeout 的情况下不发生死锁。
  1. 与上一条对应的,分布式系统应该支持 cancel 操作的几个理由:
  • 超时 超时需要取消;
  • 用户干预 当有用户驱动的并发操作时,用户可取消他发起的操作;
  • 父节点取消 就像 context 一样,父 context 取消了,子 context 也要跟着取消;
  • 重复的请求 为了得到更快的响应,同时向几个系统发起请求,当得到了最快的系统响应后,取消其他系统的请求。
  1. 可以将多个 ratelimiter 组合在一起,提供更有表达力的 ratelimiter。例如我可以限制每秒 1 个请求,同时每分钟限制 10 个请求。具体见第五章 Rate Limiting 小节。
  2. Go 使用 fork-join 模型。fork 即 go func(){}(), 而 join 则一般是指 sync.WaitGroup 或 channels。
  3. 在一个函数里(位于某个 goroutine)不断地执行 go func(){}() 语句时,会不断地产生相应的 goroutine,并被添加到当前 goroutine 所在的 P 上的 LRQ 中,LRQ 可以看作是一个双端队列,越靠近队列尾的 goroutine 和当前 goroutine 的空间局部性越紧密,越需要优先执行。基于这点考虑,新产生的 goroutine 并不是直接放到 LRQ,而是会先放到 P 的 runnext 字段,执行完当前 goroutine 或当前 goroutine 被 park 后,首先执行的就是这个 runnext。如果之后又有新创建的 goroutine,它又会把当前挂在 runnext 上的 goroutine 顶到 LRQ 中。P 执行的时候从队列头的 goroutine 开始执行,而当 steal-working 发生时,也总是先从 LRQ 的头部偷,其实就是 FIFO。

最后,全书读起来还是挺顺畅的,所需要的知识也并没有超出我现有的认知,笔记也并不多,总算是完整地读完了第一本全英文的书吧,期待后面读更多。

参考资料

[1]

英文资料: https://xargin.com/how-to-learn/

[2]

书单: https://xargin.com/readings/

本文分享自微信公众号 - 码农桃花源(CoderPark),作者:饶全成

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 生生世世 —— schedule 的轮回(七)

    上一讲,我们讲完 main goroutine 以及普通 goroutine 的退出过程。main goroutine 退出后直接调用 exit(0) 使得整个...

    梦醒人间
  • 深度解密Go语言之context

    Go 语言的 context 包短小精悍,非常适合新手学习。不论是它的源码还是实际使用,都值得投入时间去学习。

    梦醒人间
  • 千难万险 —— goroutine 从生到死(六)

    上一讲说到调度器将 main goroutine 推上舞台,为它铺好了道路,开始执行 runtime.main 函数。这一讲,我们探索 main gorouti...

    梦醒人间
  • linux系统终端命令提示符设置(PS1)记录

    PS(Prompt Sign)指的是命令提示符,在日常运维工作中为了方面操作管理,有时会设定PS1环境变量。 废话不多说,下面开始记录下Linux中PS1设置 ...

    洗尽了浮华
  • Java环境之JDK配置

    特别说明:本笔记均以Win10环境为主,Win10与Win7差别不大,个别地方有Win7的单独截图和说明,小伙伴们有疑问的地方都可以在我们的QQ群中提出来,我们...

    老九学堂-小师弟
  • Java API:Object class

    云飞扬
  • STM32终于可以免费使用ThreadX全家桶了

    从ThreadX开源那会起,就一直想深入研究这个系统,因为实在是太强了,所以还是非常想通过教程的形式推荐给大家。

    armfly
  • STM32终于可以免费使用ThreadX全家桶了

    从ThreadX开源那会起,就一直想深入研究这个系统,因为实在是太强了,所以还是非常想通过教程的形式推荐给大家。 但这个里面有个很大的问题,开源...

    armfly
  • 图覆盖准则

    有了图,我们如何来覆盖它,需要一些规则。通常我们可以进一步去扩展,一个子图可以从这一个点可达,是指从这个点出发,我们存在这么一条路径,到达这个子图,这个概念叫...

    归根落叶
  • JDK的正确安装和配置

    JDK的全称是Java Development Kit,翻译成中文就是Java开发工具包,主要包括Java运行环境、一些Java命令工具和Java基础的...

    分享达人秀

扫码关注云+社区

领取腾讯云代金券