最近写了几个 HTTP GET 相关的程序,一根筋的移数据枯燥了,于是就是多个 goroutines 去 GET 数据,最后其中一个程序从 1min 的执行时间减少到 10s 以内, 顿时觉得编写迸发程序瞒好玩的。
记得第一次有写迸发代码的意向是在大学期间看了一本经典的计算机类的书籍时(好像叫做计算机科学… 什么), 留意到了一个简单的累加计算的程序,就是从 1 累加到一个很大的数,方法有:
- 如果一根筋的从 1 累加到 n, 这是最基本的思想,同时也是最低效的方法
- 如果把 1 到 n, 划分成 m 段,开 m 个计算单位(进程或者线程或者协程都可以,process,thread,coroutine), 最后再把每一段的和值累加,这样可以大大减少等待运算结果的时间。打个现实一点的比方:如要把 n 个货物从 A 岸海运到 B 岸,只用一艘船一次只能运一个货物, 那么要耗费 n 个往反, 如果运用 m 艘船, 那么只要 n/m 或者 (n/m)+1 个往返
这可以说是可迸发化和并行化的问题了,说到迸发和并行,有必要理清一下:
- 迸发,concurrency: 着重于动作的执行,在保证结果一致性的前提下,使过程最大密集化, 如 CPU, 一个工作流可能只会点用到 20% 的 CPU 运算量, 但如果提高 n 个迸发量 CPU 的运算量就会随之被占提升到 n 倍;现实中比如一个收银员一个小时内内可以接待 10 个客人, 但实际中放流只是一个小时放 2 个客人, 这样收银员在一个小时都有 8 个接待客人的闲暇时间了, 但老板想想这样不划算, 必须要尽可能的榨干员工的劳动力, 毕竟做多做少都要支付一样的薪水, 所以就开流, 一下把 2 个 / 小时提高到 8 个 / 小时的客人迸发量进来, 这样老板就可以获取到最大的收益;在这里, 收很员可以比作 CPU, 客人就当是计算任务, 老板就是软硬件投资者。
- 并行,parallelism: 与迸发相比,着重于物理属性的呈现,比如 cpu 核心数, 接待客人的窗口数;在单核 CPU 上可以实现迸发运作, 多核 CPU 可以提高迸发的效果, 如一收银员一个小时可以从服务 2 个客人到 10 个不等, 数量越大迸发量也就越大;但是并行只能在多核 CPU 条件下呈现。如上面例子所提到的划分成 m 个单位, 单位这个属性是一样的。并行可以解决迸发达到瓶颈后的唯一支援, 如当一个收银员一个小时已经处于服务 10 个客人的情况下, 只能通过增加收银员来并行的处理大量客人才能解决及时处理大量收款的问题。
个人感觉写并发程序最主要的有三个大的方面:
- 一个就是动态的控制迸发的数量,需要对处理的数据进行一个逻辑的规划,比如说在有多核 CPU 并行支持的条件下,合理设置迸发的数量,把每个迸发单元绑定到特定的核心上, 减少迸发单元在核心之间的切换, 避免多过的上下方处理。
- 另外一个就是构建能够独立工作的模块,并且这些模块之间是没有任何联系的,当这些模块的输出有数据的交汇的时候就要进行一个锁的控制。
- 还有一个大的方面就是调试,写并发程序最难的可以说是调试了,一个 tip 就是最小化数据调试。就比如说你要处理 1000 万个数据,但是,你可以把它缩小成处理十个或者 100 个,在处理十个或者 100 个的过程中,进行一个覆盖完整的逻辑测试。
最后提一下锁,在 golang 里面有 Mutex 和 RWMutex 这两种排斥环境,Mutex 锁 (mutex.Lock) 和 RWMutex 中的 WMutex(rwMutex.Lock) 功能一样,都是对锁对像的完全占用;
RMute(rwMutext.Rlock) 允许读而只会锁住对锁对像的写操作, 所以说 RWMutex 在一定情况是比 Mutex 要高效一点(当有多个 rwMutex.Rlock 和少数 rwMutex.Lock 迸发申请时,golang 会适当让 rwMutex.Lock 优先, 避免锁对像被读锁长时间占用而无法应用写的情况)