并发的的概念错综繁杂,herb sutter就曾用盲人摸象来比喻过,这个寓言对国人来说再清楚不过了,每个人对并发及其相关的概念都有自己的见解,并为此争论不休,却几乎都是只见树木,不见森林
什么场景下会用到并发?如何对这些场景及其涉及相关概念进行分类,抽象?如何根据分析结果从以往的经验(自己的以及他人的)中找出相对应的解决方案,或者构造出新的解决方案?这篇序言将会解决前两个问题:
并发场景分类:1.需及时响应的场景 2.对吞吐量或可伸缩性有要求的场景 3.对数据一致性有要求的场景
1.需要及时响应的场景
这种场景一般出现在GUI程序中,比如编译程序,打印程序,浏览器等,用户点击编译,打印按钮,或者点击一个链接后,你不会想让界面停止,进入相应的功能函数(background work),直至返回。如果这样,界面就会假死(white screen of death),在这种情况下,用户会不停的点击程序,然后认为程序已经挂掉(实际上程序最终会返回),最后结束进程。即使用户没有结束进程,积攒下来的未能相应的用户操作也会在功能函数返回的一瞬间全部执行,这对程序的最终的正确运行也会有很大的影响,所以,永远不要让这种情况发生。
在这种情况下,我们一般会为background work分配一个新线程去执行它,或者把他作为任务(task)提交到线程池(thread pool)中,或者采用active object 模式,我会在以后的系列中陆续介绍这些。
2.对吞吐量(throughput)或伸缩性(scalability)有要求的场景
这种对吞吐量有要求的场景一般出现在需要对数据集合做大量的操作的程序中,在这种情况下,我们期望实现数据或操作上的并行,充分利用所能利用的CPU资源,以求能尽快地完成操作。
而对伸缩性有要求则是指根据不同的情况,例如输入数据的规模等来采取不同的策略划分任务
在这种情况下,我们一般会采用一些库如OpenMP 来替我们完成并行,或者自己完成并行,采用thread pool 或者 work stealing 等
3.对数据的一致性有要求的场景
这种场景一般出现在有共享资源的情况下,要求资源不能任意篡改(corruption),也不能因此造成死锁(deadlock).目前的通用办法就是对可变的共享对象(mutable shared object)加锁,或者在某些可以实现的地方采用原子变量(atomic variable),或者想数据库运用那样定义transaction 操作
以上三种场景并非绝对分离,例如编译程序在编译时为了响应用户采用1中的方法,将界面与编译(background work)分离在不同的线程中,而编译功能本身是计算和操作密集的任务,可以采用2中的方法实现更快的编译,和针对不同的待编译工程大小实现伸缩性调整,接下来的系列中我会逐渐介绍各种并发设计模式,以及他们适用于那些场景
[1] herb sutter."The Pillars of Concurrency"(http://www.drdobbs.com/parallel/the-pillars-of-concurrency/200001985)
[2]Douglas C. Schmidt ."POSA2"
[3]http://en.wikipedia.org/wiki/Active_object