《冒号课堂》连载之九——并发范式:合作与竞争
2.4 并发范式——合作与竞争
在合作中竞争,在竞争中合作。
——《竞合》
关键词:编程范式;并发式编程;线程;进程;过程式编程;函数式编程;逻辑式编程;对象式编程
摘 要:并发式编程简谈
预览
有谁愿意驾驶一辆启动后不能刹车、不能倒车、不能变速、油尽方停的汽车呢?
并发式编程以进程为导向、以任务为中心将系统模块化。
并发式编程以资源共享与竞争为主线。
提问
并发式编程有何特点?
并发式编程有哪些好处?
合理的并发式编程设计应满足哪些条件?
讲解
逗号好奇地问:“还有其他类型的编程范式吗?”
“不但有,而且有很多。”冒号喝了一口水,悠悠地说,“并发式编程(concurrent programming)就是其中之一。”
叹号有些惊讶:“并发式编程也算一种范式?它似乎更像提高运行效率的一种手段。”
“大谬不然。”冒号摇摇头,“真正的并发式编程,绝不只是调用线程[1]API或使用synchronized、lock之类的关键字那么简单。从宏观的架构设计,到微观的数据结构、流程控制乃至算法,相比通常的串行式编程都可能发生变化。随着硬件性能和用户需求的双重提升,并发式编程已成为不可回避的主题。毫不夸张地说,并发式编程是继OOP之后又一场思想和技术上的革命。只是相比OOP,尽管年龄相仿,但语言上不够支持,标准上不够统一,理论上不够完善,因而这场革命更具破坏性和建设性。现在我们来看一个例子,比较两种烧水泡茶的方案。”
说着冒号在黑板上写下——
方案一:洗茶杯;放茶叶;灌水壶;烧水;水开后泡茶。
方案二:灌水壶;在烧水的同时,洗茶杯;放茶叶;水开后泡茶。
引号见多识广:“我记得这好像是运筹学中的例子,显然方案二更佳。从编程的角度来看,方案一是串行式编程,方案二是并发式编程——烧水的线程与洗茶杯、放茶叶的线程是同时进行的。”
“如果方案一也用并发式编程呢?”冒号追问。
引号一愣,随即道:“必须先洗茶杯后放茶叶,洗茶杯放茶叶的同时也没法烧水,至于泡茶,更得等水开之后了。”
句号明白了冒号的用意:“这就是说,单凭并发式编程并不能保证提高程序性能,还必须在程序设计上下功夫。”
问号继续深究:“即便如此,如果硬件不支持,并发式编程也未必能提高效率啊。比如在一台单处理器的主机上,多个线程只能是模拟的、逻辑上的并行,而非真正物理上的并行。”
“这话有一定道理。”冒号有限度地表示赞同,“但设计者应该未雨绸缪,总不能等到有了多处理器才想到并发式编程吧?再者,没有多处理器,是不是可以利用多台单处理器的主机同时运算呢?退一步讲,即使在一台单处理器的情况下,并发式编程也是必不可少的。它能保证不同用户、不同程序之间的公平竞争,这对多用户、多任务的系统而言尤为重要。此外,采用并发式编程同样可能提高性能。最典型的一个例子:当一个线程因为等待某种资源而被堵塞时,可以切换到其他线程而不至让CPU闲置。更重要的一点是,难道除了缩短程序运行时间,并发式编程就没有别的好处吗?譬如打开一个网页,你是希望浏览器边下载边显示网页呢,还是下载完成后再显示?”
“当然是边下载边显示啦。”叹号毫不犹豫地说,“如果每个网页都是下载完成后再显示,那还不把人给憋坏了!”
逗号加了一句:“浏览器加载文字和加载图像也应分开在不同的线程,如果用户对文字部分不感兴趣,不等图像显示就可直接关闭网页了。”
“如果网页被提前关闭,那也是假用户之手变相缩短了程序运行时间。”冒号接过话来,“还有一个常见的例子,包括浏览器在内的许多应用软件在运行中都会显示一个进程条。该进程条的更新需要一个单独的线程,从效率上看它起的作用是负面的,但大大提高了用户体验,是软件人性化的表现。再举一例,媒体播放器一般都会提供暂停、快进、倒退、快放、慢放等按钮,如果不采用多线程,它们将形同虚设。此处并发式编程的作用在于提高了软件的响应能力,也改善了用户体验——有谁愿意驾驶一辆启动后不能刹车、不能倒车、不能变速、油尽方停的汽车呢?”
“岂止是不愿意,简直是恐怖!”叹号加重了语气。
“对于操作系统、各种实时系统和诸如数据库、网络服务器等基于服务的系统来说,在响应用户请求的时间上和资源的合理分配上有着极高的要求,并发式编程更是不可或缺。”句号接着补充。
问号紧接着又问:“并发式编程还有其他用处吗?”
冒号应道:“不同编程范式采用不同的视角和方法来设计和开发软件。并发式编程以进程为导向(Process-Oriented)、以任务为中心将系统模块化。我们都知道,模块化在编程中是个好东西。”
引号心存疑惑:“过程式中引入了函数模块,对象式中引入了对象模块,但并发式似乎没有在语法上引入新型模块,比如C和Java中的线程其实不过是函数或方法的包装而已。”
“我猜你的意思是:在定义一个线程之前,其主函数已经模块化了,不能把功劳记在并发式编程上,对吧?”冒号笑问,“你不能孤立静止地看待每个模块,还要考虑到模块之间的相互关联和作用。相比串行式,并发式在模块之间引入了新的通信和控制方式。也就是说,原先的一些模块的定义和划分一定是建立在线程机制的基础上的。如果失去线程的支持,它们的合理性自然会打上问号,说不定整体设计都会受到牵连。这也体现了编程范式的渗透性和全局影响力。”
教室上空弥漫的疑云一消而散。
冒号进一步指出:“除了用户主观上的需求和硬件客观上的可能外,并发式显得格外重要的另一深层原因是许多程序是现实世界的模拟,而我们生活的世界不折不扣是并发式的。从某种意义上看,并发式的模拟比对象式的模拟更贴近世界。”
引号再次提问:“并发式编程在设计上有什么要求?”
“并发式编程以资源共享与竞争为主线——又是对当今世界形势的一个逼真模拟。这意味着程序设计将围绕进程的划分与调度、进程之间的通信与同步[2]等来展开。合理的并发式设计需要诸多方面的权衡考量。”冒号说着,放出一段幻灯片——
软件易于重用、维护、测试;
公平有效地利用资源,优化程序性能如增大吞吐量、减少响应时间、提高效率等;
保障进程安全,防止竞态条件(Race Condition);
保持进程活性,避免死锁、饥饿、活锁、资源枯竭等;
减少锁开销、上下文切换等带来的性能损失;
妥善处理多进程在算法、调试等方面带来的复杂性。
叹号蹙眉:“并发式编程好是好,就是太复杂。”
“天下没有免费的午餐。有所得,必有所失。” 冒号淡淡地说,“并发式编程当然不容易,但也并非难以掌握。最重要的是,作为一个程序员,你不得不面对它。即使不直接用并发式编程,你依赖的代码和依赖你的代码也可能用到;即使现在没有用并发式,将来也可能用到。如果采取避而不理的鸵鸟政策,早晚会被人点中你的死穴。虽然目前不会对并发式编程作更深入的探讨,但希望能引起你们足够的重视。”
句号谈及他的感受:“相比OOP在语言上得到的支持,并发式的支持力度好像很不够。”
冒号点头称是:“这是由并发式的复杂性和成熟度决定的。另外,在究竟是在语言级别上支持并发,还是交由操作系统处理的问题上,仁者见仁,智者见智。Ada、Java和C#等选择前者,在语法上对并发式编程有一定的支持;C和C++等选择后者,除了关键字volatile外,主要靠库函数支持。专门为并发式而设计的语言大多仅限于学术研究而非商业应用,Erlang语言[3]是少数的例外。顺带提一下,前面提到的声明式语言具有无副作用的特性,尤其适合于并发式编程。”
问号提了一个听似奇怪的问题:“并发式与前面提到的对象式有无共通之处?”
“并发式与对象式虽是互相正交的两种范式,倒真有些相通呢。”冒号回答,“它们均与3大核心范式正交,并且越来越广泛地向它们渗透着;均为传统编程的一种推广——并发式进程的个数为1时即为传统的串行式编程,对象的方法个数为零即为传统的数据类型;均将整个程序系统分解为若干独立的子系统,不同的是一个以任务为单位,一个以对象为单位;子系统之间均能交流与合作,不同的是一个以竞争为主题,一个以服务为主题。如果将程序系统视作公司,那么并发式系统是产品型公司,每个进程是一名工人,其职责是执行单一任务;对象式系统是服务型公司,每个对象是一名服务员,其职责是提供系列服务。由此可见,一名优秀的程序设计师也应该是一名优秀的管理者。”
句号提出:“迄今为止我们谈到了5种范式,能否对它们简单概括一下?”
“你提问的时机把握得不错嘛。”冒号开始如数家珍,“为便于记忆,我们不妨用‘一二三四五’来概括编程范式之最:最传统的1个是命令式;最基本的两个是命令式和声明式;最核心的3个是命令式、函数式和逻辑式;最主要的4个是命令式、函数式、逻辑式和对象式;最重要的5个是命令式、函数式、逻辑式、对象式和并发式。最后,我们来对比一下5大范式。”
少顷,黑板上出现几行排比句——
过程式:以过程为模块的君主体系,模块之间互相授命与听命。
函数式:以函数为模块的数学体系,模块之间互相替换与合成。
逻辑式:以断言为模块的逻辑体系,模块之间互相归纳与演绎。
对象式:以对象为模块的民主体系,模块之间互相交流与服务。
并发式:以进程为模块的生产体系,模块之间互相竞争与合作。
“说回并发式编程,它要求我们摆脱以往习惯的按部就班的思维方式,对编程提出了更高的挑战。程序世界与现实世界一样,呈百舸争流、千帆竞发之势,不进则退啊!”言讫,冒号宣布,“第2堂课到此为止,欢迎下次光临。”
总结
并发式编程以进程为导向,以任务为中心,以资源共享与竞争为主线。
并发式编程有助于提高运行效率、充分利用资源、提高软件的响应能力、改善用户体验、保证公平竞争,同时以进程为单位将系统模块化,更加真实地模拟现实世界。
合理的并发式设计应该做到:软件易于重用、维护和测试;有效地利用资源,优化程序性能;保障进程安全和活性;减少性能损失和复杂度。
对象式和并发式在传统编程的基础上,分别从不同的方向进行拓展:对象式在数据类型上进行推广——允许运算作为数据类型的成员;并发式在执行顺序上进行推广——允许不同运算的执行在时间上交替或重合。它们与3大核心范式一道,组成了最重要的5大编程范式。
5大重要范式对比,如表2-2所示——
表2-2
范式 | 体系 | 模块 | 模块关系 |
过程式 | 君主体系 | 过程 | 授命与听命 |
函数式 | 数学体系 | 函数 | 替换与合成 |
逻辑式 | 逻辑体系 | 断言 | 归纳与演绎 |
对象式 | 民主体系 | 对象 | 交流与服务 |
并发式 | 生产体系 | 进程 | 竞争与合作 |
参考
[1] Abraham Silberschatz,Peter Galvin.Operating System Concepts,5ed..Reading, MA:Addison-Wesley,1998.155-235
插语
[1]并发式编程中以进程(process)或线程(thread)为基本单位。尽管它们有很大的差异,但为简明起见,这里不加区分地交替使用两个术语。
[2]同步(synchronization)只在采用共享内存(shared memory)的并发模型中需要,在采用消息传递(message passing)的并发模型中并不需要。本文主要以前一种并发模型为讨论对象,它也是大多数语言或库(包括C、C++、Java、C#等)所支持的模型。
[3]Erlang是由爱立信开发的一种通用编程语言,支持函数式和并发式,采用消息传递的并发模型。
欢迎转载,转载时请注明:
本文出自电子工业出版社博文视点(武汉)新书《冒号课堂——编程范式与OOP思想》。
http://www.china-pub.com/196068&ref=ps
http://www.douban.com/subject/4031906/