无堵塞的并发编程

11-10-06 banq
                   

顺序编程非常普及,可以说是大多数程序员编程范式,只不过可能他们没有意识到,如今已经进入并发编程时代,顺序编程和并发编程是两种完全不同的编程思路,堵塞Block是顺序编程的家常便饭,常常隐含在顺序过程式编程中难以发现,最后,成为杀死系统的罪魁祸首;但是在并发编程中,堵塞却成为一个目标非常暴露的敌人,堵塞成为并发不可调和绝对一号公敌。

因为无堵塞所以快,这已经成为并发的一个基本特征。

过去我们都习惯了在一个线程下的顺序编程,比如,我们写一个Jsp(PHP或ASP)实际都是在一个线程

下运行,以google的adsense.Jsp为例子:

<%
//1.获得当前时间
long googleDt = System.currentTimeMillis();
//2.创建一个字符串变量
StringBuilder googleAdUrlStr = new StringBuilder(PAGEAD);
//3.将当前时间加入到字符串中
googleAdUrlStr.append("&dt=").append(googleDt);
//4.以字符串形成一个URL
URL googleAdUrl = new URL(googleAdUrlStr.toString());
%>
<p>

以上JSP中4步语句实际是在靠一个线程依次顺序执行的,如果这四步中有一步执行得比较慢,也就是我们所称的是堵塞,那么无疑会影响四步的最后执行时间,这就象乌龟和兔子过独木桥,整体效能将被跑得慢的乌龟降低了。

过去由于是一个CPU处理指令,使得顺序编程成为一种被迫的自然方式,以至于我们已经习惯了顺序运行的思维;但是如今是双核或多核时代,我们为什么不能让两个CPU或多个CPU同时做事呢?

如果两个CPU同时运行上面代码会有什么结果?首先,我们要考虑两个CPU是否能够同时运行这段逻辑呢?

首先,上面代码中第二步是不依赖第一步的,因此,第一步和第二步可以交给两个CPU同时去执行,然后在第三步这里堵塞等待,将前面两步运行的结果在此组装。很显然,由于第三步的堵塞等待,使得两个CPU并行计算汇聚到这一步又变成了瓶颈,从而并不能充分完全发挥两个CPU并行计算的性能。

我们把这段JSP的第三步代码堵塞等待看成是因为业务功能必须要求的顺序编程,无论前面你们如何分开跑得快,到这里就必须合拢一个个过独木桥了。

但是,在实际技术架构中,我们经常也会因为非业务原因设置人为设置各种堵塞等待,这样的堵塞就成为并行的敌人了,比如我们经常有(特别是Socket读取)

While(true){

……

}

这样的死循环,无疑这种无限循环是一种堵塞,非常耗费CPU,它也无疑成为并行的敌人。比如JDK中java.concurrent.BlockingQueue LinkedBlockingQueue,都是一种堵塞式的所谓并行包,这些并行功能必须有堵塞存在的前提下才能完成并行运行,很显然是一种伪并行。

由于各种技术上的堵塞存在,包括多线程中锁机制,也是一种堵塞,因为锁机制在某个时刻只允许一个线程进行修改操作,所以,并发框架Disruptor可以自豪地说:无锁,所以很快。

现在非常流行事件编程模型,如Event Sourcing或Domain Events或Actor等等,事件编程实际是一种无堵塞的并行编程,因为事件这个词语本身有业务模型上概念,也有技术平台上的一个规范,谈到事件,领域专家明白如同电话铃事件发生就要接,程序员也能明白只要有事件,CPU就要立即处理(特别是紧急事件),而且事件发生在业务上可能是随机的,因此事件之间难以形成互相依赖,这就不会强迫技术上发生前面Jsp页面的第三步堵塞等待。

因此,在事件模型下,缺省编程思维习惯是并发并行的,如果说过去我们缺省的是进行一个线程内的顺序编程,那么现在我们是多线程无锁无堵塞的并发编程,这种习惯的改变本身也是一种思维方式的改变。

在缺省大多数是并发编程的情况下,我们能将业务上需要的顺序执行作为一种特例认真小心对待,不要让其象癌细胞一样扩散。我们发现这种业务上的顺序通常表现为一种高一致性追求,更严格的一种事务性,每一步依赖上一步,下一步失败,必须将上一步回滚,这种方式是多核CPU克星,也是分布式并行计算的死穴。值得庆幸的是这种高一致性的顺序编程在大部分系统中只占据很小一部分,下图是电子商务EBay将他们的高一致性局限在小部分示意图:

由此可见,过去我们实现的顺序编程,实际上是我们把一种很小众的编程方式进行大规模推广,甚至作为缺省的编程模式,结果导致CPU闲置,吞吐量上不去同时,CPU负载也上不去,CPU出工不出力,如同过去计划经济时代的人员生产效率。

所以,综上所述,以事件编程为范式的无堵塞并发是一种趋势,下面关键问题是哪种事件编程范式更简单,更易于被程序员理解掌握了。

相关话题:

为什么要用Event Sourcing?


                   

22
banq
2011-10-07 16:27

2011年10月06日 17:25 "@banq"的内容
,综上所述,以事件编程为范式的无堵塞并发是一种趋势,下面关键问题是哪种事件编程范式更简单,更易于被程序员理解掌握了。 ...

我在JdonFramework 6.5中做了如下探索:

分别使用领域事件Domain Events和DCI的场景对无堵塞并发进行不同级别的抽象封装。

无堵塞并发机制是使用经过实战考验的Disruptor框架,这个不多说,已经有很多帖子解释解释器原理。

在Disruptor上,我使用了领域事件Domain Events封装,因为事件是一个架构层次相对较高,容易理解使用的术语,特别是我们如果有了领域模型对象,通过它发出事件来驱动实现各种功能,也是非常自然易于理解的,这种功能运行是无堵塞并发的。

如果觉得事件层次还不是很高,那么使用DCI概念来实现,我们已经讨论了DCI和四色原型等软件分析方法可以顺利对接,由此可见DCI是一个几乎无技术概念,纯粹业务领域概念的高层次抽象,我用DCI封装了Domain Events,而Domain Events的底层机制是使用Disruptor,经过这样三层封装,无堵塞并发编程应该达到易于使用的目的。

如下图:当没有领域模型对象时,比如在它没有诞生之前,我们创建它时,这时是无法使用它发出领域事件的,因为它还不存在,那么我们以DCI的场景Context为主要操作场所;

当领域模型对象已经存在了,那么就应该以它为核心,它就是司令部,它可以自行运转发出各种领域事件,驱动外界为之服务,包括其自身数值的修改后持久化保存等等。

通过以上这种拟人化的的使用模式,基本上应该能够帮助普通程序员在不自觉的情况下直接使用无堵塞并发编程,从而实现并发编程能够被广泛易于使用的一个目的。

Jdon Framework 6.5beta发布

这里有一篇以机器人为案例,使用DDD/DCI/Domain Events三种不同方式,实现从软件分析设计到代码实现的全过程,包括最终源代码:http://www.jdon.com/jdonframework/dci.html

[该贴被banq于2011-10-07 16:30修改过]


mercyblitz
2011-10-07 17:47

Java 的阻塞(Object#wait和Condition#await)是基于条件等待,IO阻塞也是等待IO事件。我的理解事件是由一种有状态的命令对象(Source)伴随产生的。之所以要用Lock是因为Java内存模型决定的,既解决Java Heap对象的共享问题。

我粗刚看了一下相关的文章,感觉是通过事件模型来避免程序员自行管理Java并发,而是通过框架来管理线程并发,那么问题来了,多个相同的事件响应,怎么管理共享对象?

banq
2011-10-07 19:16

2011年10月07日 17:47 "@mercyblitz"的内容
多个相同的事件响应,怎么管理共享对象 ...

这个问题很好,我也希望多考虑一下来完善这种方式,由于事件都是模型对象发出的,这里共享对象可能是共享模型对象。

模型发出事件,可以把自己作为参数传入事件响应器中处理,如果同时有多个事件,那么就需要一种事件的前后顺序次序,这样保证某个时刻只有一个事件操作这个共享的对象,这实际上是事件的顺序化,模拟顺序编程。

不知你说的是不是这个意思,如果是,我可以详细谈一下如何通过事件的顺序化来实现业务逻辑要求的顺序,也就是管理共享对象。

mercyblitz
2011-10-07 21:50

2011年10月07日 19:16 "@banq"的内容
模型发出事件,可以把自己作为参数传入事件响应器中处理,如果同时有多个事件,那么就需要一种事件的前后顺序次序,这样保证某个时刻只有一个事件操作这个共享的对象,这实际上是事件的顺序化,模拟顺序编程。 ...

按照通常的理解的话,一个命令(事件)由一个线程处理,那么,最终处理的是单个线程,一般而言,比如Web交互不要求命令对象传递的顺序性。

我感到困惑的是,对于Java 内存模型(JMM)是JVM实现的一部分,按照原子操作比如AtomicX类和volatile的MB(Memory Fence)操作只能这对某个变量(或者成为内存区域)。但是如何通过匪Native方法来避免锁(其实锁区域是临界区域,对于执行代码而言是顺序相关的)来执行一段上下文操作。

4Go 1 2 3 4 下一页