做为一个终身学习者,输入和输出是必不可少的。输入多了以后,会发现不少中文文章很难读,可能还有不少错漏之处。不客气地说,输入的是垃圾,输出的只能是垃圾。前端
曹大常常说须要多看英文资料,包括各类新出的英文书、文章等等,这从他的书单也能够看出来。我本身的状况是:英文资料读的很少,英文技术书则基本就没完整地读过一本。以前在写文章的过程当中,仍是看了一些英文文章,收获很大。linux
此次尝试读一读英文技术书。可是直接读的话,常常读完和没读同样,没有什么感受。因而我尝试一种边读书边记读书笔记的方式,过程当中读到有趣的、有用的、之前不知道的地方就记下来,和你们分享。shell
这是一本 2017 年 7 月份出版的书,到今天已通过去三年了,Go 的版本也从当时的 Go 1.8,升级到了最新的 Go 1.15,变化巨大。数据库
下面是我记的笔记:安全
并发程序常常出错的一个缘由是人们认为本身所写代码的执行顺序是按书写的顺序来执行的,但在并发场景下,这显然是有问题的。并发
Atomicity,原子性。谈论原子性,必需要有一个 context。由于在一个 context 下是原子性的,但在另外一个 context 下,就可能不是原子性的了。具体的 context 多是:进程、操做系统、机器、集群……假想个例子,在一维空间中的 X 轴上,从坐标 1 到坐标 3 必需要通过坐标 2,这在一维空间中是绝对正确的。但做为活在三维空间里的人,我有不少种办法不通过 X 轴上的坐标 2 而到达坐标 3。仅管个人轨迹映射到 X 轴上仍是会“通过”坐标 2,这也更像一个“降维打击”的例子。分布式
造成死锁的四个条件:Mutual Exclusion(并发实体任意时刻独占资源)、Wait For Condition(并发实体同时持有资源并都在等待其余资源)、No Preemption(资源只能被持有它的实体释放)、Circular Wait(循环等待,a 等 b,b 等 c,c 等 a……)。函数
活锁是饥饿的一种,任何须要分享的资源都有可能发生饥饿,如 CPU、内存、文件句柄、数据库链接等。学习
并发(Concurrency)说的是代码,并行(Parallelism)说的是正在运行的程序。咱们没法写出并行的代码,只能写并发的代码,而且指望它能并行执行。想象一下,咱们写的代码在单核 CPU 上运行,还能并行地起来吗?测试
考察并发的代码是不是在并行执行,咱们得看在哪个抽象的层级上看:并发原语、程序的运行时、操做系统、操做系统所在的平台(容器、虚拟机……)、CPUs、机器、集群……
和前面说的 Atomicity 同样,谈论 Parallelism 时,也要有一个 context。它决定是否将能将两个操做当作并行。例如,咱们运行 2 个操做,每一个操做花费 1 秒。若是 context 是 5 秒钟,那能够说这两个操做是在并行执行;但若是 context 是 1 秒钟,那咱们认为,这两个操做是串行地在执行。注意,context 并不等同于时间,线程、进程、操做系统等均可以当作 context。
给并发或者说并行定义什么样的 context 和并发程序是否正确运行有很大关系。例如,context 是两台电脑,咱们分别在两台电脑上运行两个计算器程序,那理论上这两个计算器程序就是并行的,且不会相互影响。
在上面的例子里,context 是两台电脑,operations 是两个进程。很明显,我在个人电脑上运行任何程序,都不会影响你的电脑。可是在同一台机器上,一个进程还能保证不影响另外一个进程吗?回答是不必定,好比读写同一个文件……
大部分程序的并发抽象层级是线程。Go 在抽象层级上又增长了一个 goroutine。按理说,层级层次越高,并发安全性越难保证。但实际上 goroutine 让事情变得更容易,由于它并非在线程的抽象层级之上又加了一层,而是取代了线程。
Go channel 的设计思想来源于 Hoare 于 1978 年发表在 ACM 上的一篇关于 CSP(Communicating Sequential Processes)的论文。Go 是第一门吸取了 CSP 精华而且将其发扬光大的语言。
大多数语言使用线程+并发同步访问控制做为并发模型,而 Go 的并发模型由 goroutine 和 channel 组成。线程相似于 goroutine,而并发同步访问控制则相似于 mutex。
Go 并发的理念是:简单,尽可能使用 channel,尽情使用 goroutine。
在 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。
使用 sync.WaitGroup 时要注意,sync.Add 要在新起 goroutine 语句的外层调用,不然执行到 sync.Wait 时,可能新起的 goroutine 还没调度到,sync.Add 天然没执行,最终致使逻辑出错。
mutex 是 mutual exclusion 的简写,翻译一下:互相排斥。
sync.cond 有两个比较有意思的方法:sync.Cond.Signal 和 sync.Cond.Broadcast。前者会唤醒等待时间最长的 goroutine,后者会唤醒全部等待的 goroutine。另外,要注意 sync.Cond.Wait 方法内部,隐藏了一些反作用,会先解锁:c.L.Unlock()
,而后再加锁:c.L.Lock()
。
查询 Go 源码使用了多少次 sync.Once:
grep -ir sync.Once $(go env GOROOT)/src | wc -l
channel 是粘合 goroutine 的胶水,select 则是粘合 channel 的胶水。
关于 runtime.GOMAXPROCS(n) 函数的一个可能的使用场景:代码中可能存在 data race 的状况,增长 n 值可让 data race 更快地发生,从而能够更快地调试错误。
为了不 goroutine 泄露,请注意:生成子 goroutine 的父 goroutine 须要负责中止子 gotoutine,即谁建立谁销毁。
能够将一个“无序、耗时长”的 stage 转成 fan-out。fan-in 是多转一,fan-out 则是一转多。
设计系统的时候,应该一开始就考虑 timeout 和 cancel。
分布式系统须要支持 timeout 的几个理由:
饱和
系统饱和时,最后到达的请求须要直接超时返回,不然可能引起雪崩;
数据过时
数据其实有必定的时间窗口,过了窗口,就是无效数据了。例如前端一个请求过来,假设用户能够容忍 2s,那这个窗口就是 2s,分布式系统须要支持 2s 的超时设置,超过 2s 后数据无效;
防止死锁
固然,触发 timeout,有可能使死锁变成活锁。系统设计的目标应该是在不触发 timeout 的状况下不发生死锁。
超时
超时须要取消;
用户干预
当有用户驱动的并发操做时,用户可取消他发起的操做;
父节点取消
就像 context 同样,父 context 取消了,子 context 也要跟着取消;
重复的请求
为了获得更快的响应,同时向几个系统发起请求,当获得了最快的系统响应后,取消其余系统的请求。
能够将多个 ratelimiter 组合在一块儿,提供更有表达力的 ratelimiter。例如我能够限制每秒 1 个请求,同时每分钟限制 10 个请求。具体见第五章 Rate Limiting 小节。
Go 使用 fork-join 模型。fork 即 go func(){}(), 而 join 则通常是指 sync.WaitGroup 或 channels。
在一个函数里(位于某个 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。
最后,全书读起来仍是挺顺畅的,所须要的知识也并无超出我现有的认知,笔记也并很少,总算是完整地读完了第一本全英文的书吧,期待后面读更多。