并发编程模型和访问控制
http://www.eecs.harvard.edu/~mdw/papers/seda-sosp01.pdf, paper
Staged Event Driven Architecture (SEDA) 介绍
并发的编程模型
1. 多线程模型
最为传统的模型, 为每个request起个新的thread去执行, 以实现并发
最大的问题是, 扩展问题, request很多的时候, 太多的thread会产生很大的调度耗费, 当然可以使用线程池来进行优化
这种模型比较适合于CPU密集的request, 并设定近似于CPU核数的线程数, 达到并发计算的效果
还有就是并发粒度问题, 只能在request级别进行并发
2. 事件驱动模型
为了解决并发粒度问题, 思路是把一个request拆成N个stage, 每个stage都单独一个FSM线程(有限状态机), 并依赖中央scheduler进行统一协调
当request到达的时候, 通过schdeuler依次发送event, 完成N个stage
这样解决扩展性问题, 因为是基于stage级别去并发, 所以就算有10000个request, 仍然只需要5个stage线程
再者, 并发粒度比较小, 对于request中不同的stage可以设置不同的并发度
当然这个方案问题很明显, 耦合度和复杂度比较高, 不同类型的request都需要实现一系列的FSM线程, 并且scheduler的实现会比较复杂
对比现在说的比较多的event-drive模型, 和这个还是有差别的
对于现在越来越多的web应用, 大部分应用都是I/O等待密集的request, 这种case使用多线程方式是非常低效的
所以需要使用event-drive模型, Reactor模式, 用一个线程去等待10000个request和用10000个线程去等待, 效果上没有区别, 但是耗费上却天壤之别
当然这种基于单线程的reactor模式, 只能节省I/O等待时间, 但对于CPU计算密集型的request因为是单线程, 所以就相当于串行执行
3. SEDA或Actor模型
没看出SEDA和Actor两种模型的差别, 思路和方法基本一致
和event-based模型的关键不同, 就是去耦合和去中心化
对于event-based, 需要scheduler知道所有的过程, 负责所有的event的发送和协调
但对于SEDA, 完全的去耦合, 对于任一个stage, 都是独立的, 完全可重用的, stage之间完全通过event进行沟通
对于每个stage只需要知道, 下级stage是谁(将数据发送给谁), 开发人员可以任意配置outgoing events, 从而形成workflow
对于stage级别的并发度, 通过设置thread pool就可以简单的设定
当然问题是, 依赖event queue, 效率上有些问题, LMAX Disruptor就是来解决这个效率瓶颈的
并发访问控制
这个问题经常会和上面的问题混合在一块, 这样不清晰
任一种并发的编程模型都会有访问控制问题, 解决这个问题的方法其实也很简单, 加锁.
1. 悲观锁
完全互斥锁, 我做的时候, 你等着, 我做完, 你再做
简单, 问题效率低
2. 乐观锁
妥协一点, 各自做各自的, 只在最终提交的时候, 去check当前的状态是否已经变化, 如果已经变化那么提交失败
根据最新的状态, 处理逻辑后重新提交
3. MVCC (Multi-VersionConcurrencyControl)
另一种思路, 不加锁, 各写各的, 所以我们可以同时保存相同数据的不同版本, 然后在client query的时候返回所有的版本, 让client自己去决定选取什么版本
Nosql常常采用这种方式, 当然这个方法明显加重了client的负担
4. STM (Software Transactional Memory)
STM其实是综合了MVCC和乐观锁机制, 比较典型的案例是, CouchDB和Clojure
用通俗的描述解释一下,
首先是MVCC, 大家都可以并发的随意的在自己的版本上修改数据, 当然你的数据别人是看不得的
然后是乐观锁, 当你想要commit的时候, 这个时候是需要加乐观锁的, commit的过程其实就是将公共的reference指向你的版本
为什么是transactional?
因为你可以在你的版本上做很多改动, 但仅仅当reference切换成功后, 所有的更新会被可见
如果reference切换失败, 所有的更新都不会被可见
通过简单的机制就是避免的rollback等复杂的操作, 但是却达到了和transaction同样的效果
所以就算你的更新没有成功或由于crash丢失了, 至少不会影响原来数据的一致性