简评:并发问题的牛鼻子
Concurrency 的牛鼻子是 shared data,找准了 shared data 基本上就解决了一大半的问题。很多时候,意识不到 concurrency,或者无法利用 concurrency 的加锁性质,就在于无法正确识别 shared data。
如果一个 concurrent 的程序,压根儿就没有 shared data,那么恭喜你,这就意味着你完全不必考虑 concurrency conflict,因为没有什么地方会有交集,自然也就不会用冲突。所谓井水不犯河水,哪来的冲突。
但是,这同样意味着:如果你想利用 concurrency conflict 来加锁去做「顺序化」或「排他性」,是办不到的。例如,听起来高大上的「分布式锁」,其本质就在于不同的 server 之间,根本没有 shared data,于是,你根本无法控制 server 之间的顺序化/排他性。而解决的方案也非常简单,就是为这些不相关的 server 引入一个 shared data 就可以了。
很多了解不透彻的人,一谈到分布式锁似乎就必须使用 Zookeeper、使用 Redis。而事实上呢,一切可以提供 shared data 的 service 都可以。例如使用 db 中的一个字段,甚至使用某个单机服务的变量做 shared data。要点不在于使用的是什么中间件,而是需要有一个 shared data 供这些 server 访问。
再来,很多时候意识不到或者弄错了 concurrency conflict,就在于把 shared data 弄错了。例如典型的 ++i 这个操作,从代码层级看,似乎不同的 thread 之间没有 shared data。可是,如果你从 CPU 的 instruction operation 的角度来看,这个操作会有 shared data 残留在不同 thread 之间共享的 memory 中。
所以,要识别清楚 shared data,一个非常核心的要点就在于你必须对「底层」机制特别清楚。因为如果无法知晓底层机制,那么这就意味着这一块 code 往下都是黑箱。你自然是无法判别黑箱之中是否有 shared data 存在。
总之,找到了 shared data 就找到了 concurrency problem 的坐标系。大到分布式系统的千百台 server,中到一个 project 中数十个代码变量,小到 CPU 的 instruction operation,它们都可以被归结为同一类问题,即:什么是你要解决的问题的 shared data,这些 shared data 可由哪些操作改变。有了这个坐标系,一切分析问题和解决问题的工作才得以开展,是以不变应万变的根本。
另:解决 concurrency conflict 的方式其实很自然,就是让访问 shared data 的操作强制有序,也就是排队。就像大家都要去抢占公园入口这么个 shared data,只需要在这个入口前方构建一个队列即可。特别地,所谓的「加锁」,无非就是长度为1的队列罢了。
如此来看,解决 concurrency conflict 的方式也极其简单粗暴,就是让它无法做 concurrent 操作,即:拉出队列做「顺序化」。