Go 语言并发之道
Go 语言的并发哲学
1、你想要转让数据的所有权吗?
如果你有一块产生计算结果并想共享这个结果给其他代码块的代码,你所实际做的事情是传递了数据的所有权。可以创建一个带缓存的channel来实现一个低成本的在内存中的队列来解耦生产者和消费者。
2、你是否试图在保护某个结构的内部状态?
通过使用内存访问同步原语,可以为你的调用者隐藏关于重要代码块的实现细节。
3、你是否试图协调多个逻辑片段?
请记住,channel本质上比内存访问同步原语更具可组合性。
4、这是一个对性能要求很高的临界区吗?
如果程序的某个部分,事实证明是主要的性能瓶颈,比程序的其他部分要慢几个数量级,使用内存访问同步原语会有帮助。
Go 语言的并发性哲学可以这样总结:追求简洁,尽量使用channel,并且认为goroutine 的使用是没有成本的。
Go 语言并发组件
goroutine
main 也是一个 goroutine。简单地说,goroutine 是一个并发的函数,与其他代码一起运行。可以简单地在一个函数之前添加 go 关键字来触发。同样可以作为匿名函数来使用。
goroutine 是如何工作的?OS线程?绿色线程(由语言运行时管理的线程)?我们能创造多少个goroutine?
Go 语言中的 goroutine 是独一无二的。它们不是OS线程也不是绿色线程,它们是更高级别的抽象,称为协程,是一种非抢占式的简单并发子goroutine,也就是说它们不能被中断。取而代之的是,协程有多个点,允许暂停或重新进入。Go 语言的 runtime 会观察 goroutine 的运行时行为,并在它们阻塞时自动挂起它们,然后在它们不被阻塞时恢复它们。
Go 语言的主机托管机制是一个名为 M:N 调度器的实现。将M个绿色线程映射到N个OS线程。然后将 goroutine 安排在绿色线程上。Go 语言遵循一个称为 fork-join 的并发模型。fork 这个词指的是在程序中的任意一点,它可以将执行的子分支与其父节点同时运行。join 这个词指的是在将来某个时候,这些并发的执行分支将会合并在一起。
闭包可以从创建他们的作用域中获取变量的引用。
WaitGroup
你可以将 WaitGroup 视为一个并发安全的计数器:调用通过传入的整数执行add方法增加计数器的增量,并调用done放大对计数器进行递减。wait阻塞,直到计数器为零。
cond
https://zhuanlan.zhihu.com/p/367166977
https://zhuanlan.zhihu.com/p/401242064
大部分场景下使用 channel 是比 sync.Cond方便的。不过我们要注意到,sync.Cond 提供了 Broadcast 方法,可以通知所有的等待者。想利用 channel 实现这个方法还是不容易的。我想这应该是 sync.Cond 唯一有用武之地的地方。如果要实现广播唤起效果,只需要利用 context 的链式取消特性,也能达到该效果。
为什么 cond 不能复制?
once
https://studygolang.com/articles/21852?fr=sidebar
sync.once 是一种类型,即使在不同的 goroutine 上,也只会调用一次 Do 方法处理传递进来的函数。即使传的函数不一样,Do 也只会处理第一次传入的函数。
池
如何在多个 goroutine 之间使用同一个 pool 做到高效呢?官方的做法就是尽量减少竞争,因为 sync.pool 为每个 P(对应 cpu,不了解的童鞋可以去看看 golang 的调度模型介绍)都分配了一个子池,如下图:
当执行一个 pool 的 get 或者 put 操作的时候都会先把当前的 goroutine 固定到某个P的子池上面,然后再对该子池进行操作。每个子池里面有一个私有对象和共享列表对象,私有对象是只有对应的 P 能够访问,因为一个 P 同一时间只能执行一个 goroutine,因此对私有对象存取操作是不需要加锁的。共享列表是和其他 P 分享的,因此操作共享列表是需要加锁的。
1、什么情况下适合使用sync.Pool呢?为什么 sync.Pool 不适合用于像 socket 长连接或数据库连接池?
因为,我们不能对 sync.Pool 中保存的元素做任何假设,以下事情是都可以发生的:
-
Pool 池里的元素随时可能释放掉,释放策略完全由 runtime 内部管理;
-
Get 获取到的元素对象可能是刚创建的,也可能是之前创建好 cache 住的。使用者无法区分;
-
Pool 池里面的元素个数你无法知道;
所以,只有的你的场景满足以上的假定,才能正确的使用 Pool 。sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担。划重点:临时对象。所以说,像 socket 这种带状态的,长期有效的资源是不适合 Pool 的。Pool 不适用于有状态的对象,只适用于无状态的临时大对象的复用。
channel
for range channel 时,如果 channel 已经 close 这个循环会自动退出。由于一个被关闭的 channel 可以被无限次读取,所以关闭 channel 也是一种同时给多个 goroutine 发送信号的方法。
缓冲 channel 是一个内存中的 FIFO 队列
尽量保持 channel 所有权的范围很小(创建、写入和关闭),消费者函数只能执行 channel 的读取方法。
Select
一个 select 模块包含一系列的 case 语句,case 语句没有顺序,如果没有满足任何条件,执行也不会失败。如果多个 case 语句都可以执行将会随机挑选一个。如果没有任何语句可以执行会一直阻塞,可以使用 time.After 来设置超时时间,也可以通过 default 继续执行自己的操作
GO 语言的并发模式
约束
约束是一种确保了信息只能从一个并发过程中获取到的简单且强大的方法。达到此目的时,并发程序隐式安全,不需要同步。
for-select 循环
防止 goroutine 泄露
goroutine 不会被运行时垃圾回收。
父 goroutine 传递一个只读的 channel 给子 goroutine,在想要子 goroutine 退出的时候,关闭这个 channel,子 goroutine 读取到关闭信号后退出。
如果 goroutine 负责创建 goroutine,它也负责确保它可以停止 goroutine。
or-channel
场景:期望将多个 channel 的关闭信号组合成一个 channel 的信号,只要多个 channel 中的一个 channel 关闭了,那么这个组合的 channel 也会关闭。
方法:通过递归开启 goroutine,func(channels ...<-chan interface{}) <-chan interface{},select 的 case 包含 channels 的前几项,然后递归开启这个函数监听 close 信号,收到信号后,把返回值的 channel close,即可达到目的。
构建 pipeline 的最佳实践
pipeline 是一系列将数据输入,执行操作并将结果数据传回的系统。一个 stage 消耗并返回相同的类型,
精髓:通过同构的channel,把不同的stage(也就是函数)连接起来,达到上游channel产出数据,下游立马能消费数据并接着产出结果供再下游使用的效果。
扇入,扇出
channel内部维护了一个互斥锁,来保证线程安全。
pipeline 的一个有趣属性是它们能够让你使用独立的,并且可以尝尝重新排序的 stage 组合来操作数据流。你甚至可以多次重复使用 pipeline 的各个 stage 。在多个 goroutine 上重用我们的 pipeline 的单个 stage 以试图并行化来自上游数据的 pull,可以优化耗时高的 stage,提升整体 pipeline 的性能。这种模式就叫做:扇入,扇出。
扇出是一个术语,用于描述启动多个 goroutines 以处理来自 pipeline 的输入的过程,并且扇入是描述将多个结果组合到一个 channel 的过程中的术语。
or-done-channel
如果在其他协程中调用了close(ch),那么就会跳出for range循环。这也就是for range的特别之处
Context
https://zhuanlan.zhihu.com/p/110085652
context
主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context
时有两点值得注意:上游任务仅仅使用context
通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context
的取消操作是无侵入的;context
是线程安全的,因为context
本身是不可变的(immutable
),因此可以放心地在多个协程中传递使用。
大规模并发
超时和取消
1、确保不可抢占的原子操作的运行时间在可接受的范围内,如果不可抢占的运行周期太长,可以考虑拆分成小段,小段之间加入可抢占的done channel判断,使得上游发出取消信号后,下游真正退出的时间间隔不会太长。
2、goroutine 接受到取消信号后如何处理 已经执行了的逻辑修改过的数据? 数据的修改尽可能保持在很小的范围内,并确保很容易回滚,尽可能将中间结果存储在内存。
限流
令牌桶算法:如果要访问资源,必须拥有资源的访问令牌,否则会被拒绝。
https://www.liuvv.com/p/e25fe1fc.html
https://studygolang.com/articles/34413
小技巧:可以在一个结构体里包含多种类型的限速器(访问网络、磁盘等等),每个限速器都实现限速器需要的接口(wait,limit等),这样可以按照实际场景多重包装、自由组合限速器。
goroutine 和 Go 语言运行时
https://www.jianshu.com/p/36e246c6153d
https://zhuanlan.zhihu.com/p/271706257
golang pprof 使用简介
https://studygolang.com/articles/21243