首页 > 代码库 > 软件架构(二):同步与异步问题
软件架构(二):同步与异步问题
一. 基本概念
一个大型的程序系统常常是由很多不能功能模块组成的。程序系统运行时不同功能模块要按一定顺序执行,以协同完成一件任务。功能模块协作运行完成一件任务存在同步和异步两种方式。如果在某一时间段,这个程序系统的所有功能模块都在为完成相同的一件任务而服务,某一个功能模块在完成一件任务的子任务后,需要等待其他功能模块完成子任务,这样只有当全部功能模块按顺序完成一件任务后,程序系统才能接收下一个任务,功能模块是串行运行,这称之为同步模式。反之,在某一时间段,这个程序系统的不同功能模块可以独立运行完成一件任务的子任务,无须等待其他功能模块完成子任务就可以继续处理下一件任务的子任务,功能模块是并行运行,这称之为异步模式。
打个比方,在一个生产流水线上有A和B两道工序,如果工位A完成自己任务,把任务传递给工位B后,需要等待工位B完成工序后,工位A才能开始下一件产品工作,这就是同步模式。如果工位A在把任务传递给工位B后,工位A不等待工位B是否完成任务,就开始下一件产品的任务处理,这就是异步模式。
反映在OLTP程序系统中,一个交易就是一个任务。如程序系统一次只完成一个交易,在这个交易没有完成前,程序系统不接受其他交易,这就是同步模式。如程序系统把交易任务分拆成几个独立的子进程,每个子进程独立完成交易的一个子任务,几个子进程同时运行,这就是异步模式。由于交易在模块之间是按照一定顺序运行的,所以对一个具体交易而言,模块之间任务执行时并不表现为并行运行,但对大批量交易的宏观效果而言,模块之间却是表现为并行运行。
二. 实现问题
在多功能模块的程序系统设计中,最重要的是要考虑模块之间的通信或数据传递。同步结构的程序实现很简单,常见情况是把所有功能模块编译在同一个程序中,这样实现数据共享不难,有些情况是利用函数参数传递共享数据,通过CALL调用,来执行下一个功能模块,所以同步结构的功能模块一般是不能独立运行的,但通信模块除外,因为同步的通信模块本身就可以通过SOCKET等通信机制来传递共享数据,所以是可以独立运行的。
异步结构的实现稍微复杂一些。因为要求异步结构程序系统的不同功能模块作为独立进程能够独立运行,但在非通信进程模块之间,在进程间就存在共享数据鸿沟,每个独立进程就象一个个信息孤岛,这就要求进程间能够实现通信以在进程间传递数据。
在普通进程间要实现数据共享,实际有效通信的常用IPC工具是这三种:
消息队列(Message Queue)、
共享内存(Shared Memory)
信号灯(Semaphores)
在实际应用系统开发中,消息队列使用得最多。消息队列实际上就是一个可以读写应用数据的内存块。它可以被不同的进程读写。比如下图表示有三个进程向一个消息队列写信息,有一个进程从该队列中读取信息。
图 1消息队列示意图
在消息队列的基础上,一些厂商提供了性能更好、功能更多的“消息队列”产品,即Message Q,简称MQ,如IBM公司的MQSeries(现在叫webSphere MQ)、BEA公司的MQ,国内有的软件公司也开发了自己的MQ产品。这些MQ的出现为大型异步结构程序的进程间通信设计提供了强有力的开发工具。
三. 效率问题
这里的效率狭义讲就是程序系统运行快慢问题。异步模式在速度上远远快于同步模式,这是一个软件设计和生活常识。但有些人却认为同步结构的程序效率要优于异步结构的程序,有的人也难以理解异步模式为什么会比同步模式效率高,那么我们就来具体分析同步结构程序和异步结构程序的效率优劣。
以OLTP系统为例,完成一个交易平均所需时间是评价OLTP程序系统性能的最重要指标。假设某种典型交易需要A、B、C三个顺序任务来完成,相应地有A、B、C三个功能模块。假设A、B、C三个模块完成各自任务所需时间分别是Ta、Tb、Tc。对于同步结构程序来说,系统完成一个交易所需时间理所当然应该为三个模块的简单累加,即Ta+Tb+Tc。但对于异步结构程序来说,由于程序系统被分成了几个独立进程,每个进程需要增加额外的时间s来处理进程间通信,这样异步程序完成这样一个交易所需时间应该是(Ta+s)+(Tb+s)+(Tc+s)。因为(Ta+s)+(Tb+s)+(Tc+s)>Ta+Tb+Tc,据此一些人所以认为同步程序所以比异步程序效率高。
然而,他们却忽视了一个重要问题,就是在实际业务运行时往往是大批量交易同时运行。假设现在有N笔交易需要程序系统来处理,在同步程序未进行并发机制处理条件下,由于同步程序一次只能做一个交易,交易是串行完成,所以完成N笔交易时间应该是一次交易时间的N次简单累加,即 N*(Ta+Tb+Tc),从宏观看,同步模式完成N笔交易,处理总的时间是N*(Ta+Tb+Tc),所以平均交易处理时间也就是一个交易的执行时间:Ta+Tb+Tc。如果从用户的角度来看,每个人在等待时并不理会别人是否在等待,因为他等待的时间都是属于他自己的,这样每个人的平均等待时间就是(1+2+3+..+n)*(Ta+Tb+Tc)/N= (N+1)/2*(Ta+Tb+Tc),但是这个时间并不是系统处理交易的平均执行时间。所以在理想计算机系统同步模式下,并发量对交易平均处理时间没有影响,并不会随着并发量的增大而增大。
对于异步程序来说,首先假设在多CPU计算机系统理想情况下,由于多个CPU完全能够支持各个进程同时独立运行,假设A、B、C三个进程同时接到N笔任务,就象一条生产流水线上三个工位同时处理N笔任务,由于三个进程同时运行处理,这样从宏观结果来看,程序系统平均完成一个交易所需时间只相当于那个最慢的进程所需时间,比如生产流水线上最慢的那个工位完成了N笔任务,那么实际上整个流水线也就完成了N笔任务,因为其他工位的任务早已经完成了。所费时间最大相差误差也就是一个交易或一个任务的时间,这个时间也就是流水线上最慢那个工位之前的工位在处理第一个交易的时间加上最慢那个工位之后的工位在处理最后一个交易时的时间,加起来正好是一个交易的时间。而这一个交易或一个任务的时间相对于N笔任务所费时间是可以完全忽略不计的。简单地说,两个人同时吃饭,甲吃饭花了1个小时,乙吃饭花了2个小时,那么他们都吃完饭的时间应该是2小时,而不是1+2=3小时。实际上一个餐厅工作模式就是一个异步模式,因为一个餐厅同一时间能够供许多人就餐,而不是只容许一个人吃完了才容许下一个人就餐。
图 2 异步结构程序工作示意图
假设C进程最慢,这样一来异步程序完成这N笔交易所需时间等于N*(Tc+s)+(Ta+s)+(Tb+s),平均处理时间约等于(N*(Tc+s)+(Ta+s)+(Tb+s))/N。即交易平均处理时间与并发量的关系类似于y=a + b/x。所以在多CPU计算机系统理想情况异步模式下,交易平均处理时间会随着并发量的增大而急剧下降,最终也趋向一个恒定值。但大批量情况下异步结构程序平均单笔交易处理时间显然要远远小于同步结构的程序。
图 3 多CPU理想情况下交易平均交易处理时间与并发量关系
那么在单CPU计算机系统下结果又如何呢?表面上看单CPU需要分片处理多任务,似乎完成同步结构的交易要比异步结构的交易要快,但实际上在绝大多数应用系统中,CPU利用率是很低的,通常是在百分之十几以下,这是因为应用程序通常都要花费大量时间与其他外设打交道,比如读写文件,有的应用程序自身还有额外的其他等待时间等等,这些过程是不占用CPU时间的。以此考虑,异步结构程序与在多CPU理想情况下的运行情况很相似。但毕竟受限于单CPU的处理能力,所以异步结构程序会随着自身进程数的多少和进程复杂度,其运行效率会相应下降。在有限并发量情况下,平均交易处理时间与并发量关系曲线与在多CPU理想情况下相似。
所以如果只运行一个或几个简单交易,同步结构的程序也许要比异步结构的程序快,但实际情况却是应用系统要在短时间内处理大批量的交易,所以显然异步结构的程序效率要远远快于同步结构的程序。异步结构的程序能够最大限度地利用CPU的处理能力,而同步结构的程序则在CPU处理能力的利用上造成极大浪费。尤其是在多CPU计算机系统上,每增加一个CPU,异步结构程序效率就能提高一大台阶,而同步结构程序因无法利用多CPU处理能力,所以对其效率几乎没有影响。在商业应用计算机系统中,绝大多数都是多CPU的系统,所以异步和同步结构的应用程序孰优孰劣一目了然。就象在规模大、人数多的公司内,如果再增加一个新餐厅,那么就餐效率就会大大提高一样。现在许多大型商业软件都采用异步模式的程序设计结构,为满足异步程序开发的需求,IBM、BEA等公司推出的专门用于进程间通信的MQ中间件产品就是很好的异步结构程序开发工具。
四. 并发问题
上节已经证明,在理想情况下,对于同步模式来说并发量实际上对交易平均处理时间没有什么影响。相反,在异步模式下交易平均处理时间会随着并发量的增大而急剧下降,即处理速度越来越快,最终也趋向一个恒定值。而我们常常说到的“软件能够支持最大并发量”实际上并不是效率问题,而是程序设计利用硬件资源问题!即程序设计结构和硬件资源共同限制了并发量。
在大批量业务处理情况下, 同步结构的程序必须使用并发机制才能保证程序系统的处理速度, 否则系统一次只能处理一个交易,要等待该交易完成后才能接受下一个交易,在大量交易并发情况下前台客户等待时间就会很长,即客户平均等待时间等于(N+1)/2*单笔交易处理时间,这样的系统速度在大批量交易并发情况下常常是不能被接受的。
异步结构的程序由于已经分成多个进程,每个进程都不等待地处理自己的任务,所以异步结构的程序一般情况下甚至无需进行并发处理,就可以满足大批量交易处理要求。当然异步结构的程序每个进程都可以再进行并发处理,这样每个进程的处理速度都能大大得到提高。
所谓并发处理一般是通过fork函数调用,来复制自己,产生许多个与自己一模一样的子进程来同时并行处理进入程序系统的多个交易。所以说并发程序设计实际上是一种特殊的异步结构程序设计。而所谓最大并发量往往是由系统可使用内存、MQ容量等来限制的。在同步程序结构设计中,由于多个功能模块常常编译在一个程序内,这样在进行并发处理时,程序必须复制自身全部代码。而程序代码是要占用内存空间的。假设程序系统有A、B、C三个功能模块组成,每个模块的代码量为Ma、Mb、Mc,假设同时有N个交易申请任务,如果同步结构程序采取并发机制,则程序必须同时复制N个自身,这样就有N*(Ma+Mb+Mc)代码量需要占据内存,这是相当可观的,往往交易量一大就会突破内存可使用容量。而异步结构程序通常情况下甚至无需fork自身就可满足需求,即使需要fork进程,也可以灵活地有选择地对那些速度慢的瓶颈进程进行fork, 所以占用内存代码量相对较小,且代码利用率高,不象同步结构程序的许多代码处于白占内存的等待闲置状态,所以在同样的计算机系统环境下,异步结构的程序往往比同步结构的程序具有更大的容许并发量。所以异步结构的程序能够更有效率利用硬件资源,而同步结构的程序则对硬件资源造成很大的浪费(过度浪费CPU,过度占用内存)。
在采用并发机制的程序中,应该根据程序代码量和内存、MQ等大小以及CPU处理能力等来估算最大并发量,并在程序中设置参数来进行控制。如果不加控制,一旦并发进程数超过内存或MQ的最大容许量,有可能引起如系统死机等不可预测结果。而且,并发进程如果过多,超过硬件资源允许度,则进程之间容易竞争系统资源,造成进程的拥塞,影响交易运行速度。这就象一个公司规模很大,就餐人数多,而餐厅规模小,不能同时供应这么多人就餐,那么只好限制同一时间就餐人数,用延长就餐时间来弥补。
比如在通信程序中,经常需要考虑并发连接的问题,这就需要在SERVER端设置成并发机制的服务器,需要设置最大并发连接参数。在SOCKET接口函数中已经提供了这个功能,这是通过在LISTEN函数的参数中进行设置来实现的。
假设同步进程并发处理N笔交易,在理想情况下,就是说在一个交易时间内系统处理了N笔交易,那么平均处理时间即为(Ta+Tb+Tc)/N,即平均处理时间与并发量关系类似于y=a/x曲线。在实际情况下,当并发量超过系统容许并发量后,交易处理时间会变慢,那么该曲线后半段可以修正为类似于y=a/x+bx曲线。
图 4 实际情况下交易平均交易处理时间与并发量关系示意图(不加并发量控制)
如系统进行并发量控制,那么当请求并发量超过系统控制值后,系统不会再fork新的进程,新的请求只有排队等待系统接受处理,因此无论以后请求并发量有多大,系统的平均处理时间不会再随并发量的增大而改变。
,3/(2*N1+Nx),4/(3*N1+Nx)
图 5实际情况下交易平均交易处理时间与并发量关系示意图(加并发量控制)。图中N1和N2分别表示并发模式下同步和异步结构程序设置的最大并发量阀值。
在OLTP系统中,还存在一种特殊的并发异步结构模式。由于后台大量交易基本上都是独立的程序,所以主控程序同时CALL多个不同的交易程序过程,也是一种特殊的并发异步结构模式。比如后台批处理经常是采用这种特殊的并行异步结构模式来处理。
五. 混合结构
同步异步是相对的,在一个大型程序系统中可能同时存在。比如一个银行应用系统,其前端发出一个交易请求,然后处于等待状态,等待后台返回结果,这就是同步现象,但后台处理则可能是通过异步结构来实现的。所以在一个大系统中,既可能存在同步的代码,也可能存在异步的代码。对于银行柜台业务系统来说,其通讯进程是同步结构的,但是由于后台的通讯进程与主控进程及交易进程通常是分开独立运行的,所以后台处理还是异步结构的。在实际程序设计中,需要灵活设计同步和异步结构,以达到功能和性能的最大优化。
软件架构(二):同步与异步问题