当前位置:   article > 正文

并发(1):并发的多面性_单处理器系统实现并发技术

单处理器系统实现并发技术

    到目前为止,你学到的都是有关顺序编程的知识。即程序中所有事物在任意时刻都只能执行一个步骤。

    编程问题中相当大的一部分都可以通过使用顺序编程来解决。然而,对于某些问题,如果能够并行的执行程序中的多个部分,则会变得非常方便甚至非常必要,因为这些部分要么看起来在并发地执行,要么在多处理器环境下可以同时执行。

    并行编程可以使程序执行速度得到极大提高,或者为设计某些类型的程序提供更易用的模型,或者两者皆有。但是,熟练掌握并发编程理论和技术,对于到目前为止你所学的知识而言,是一种飞跃,并且是通向高级主题的中介。本章只能作为一个介绍,即便融会贯通本章的内容,也绝不意味着你就是一个优秀的并发程序员了。

    正如你应该看到的,并行执行的任务彼此开始产生互相干涉时,实际的并发问题就会接踵而至。这可能会以一种微妙而偶然的方式发生,我们可以很公正的说,并发“具有可论证的确定性,但是实际上具有不可确定性”。这就是说,你可以得出结论,通过仔细设计和代码审查,编写能够正确工作的并发程序是可能的。但是,在实际情况中,更容易发生的情况是所编写的并发程序在给定适当条件的时候,将会工作失败。这些条件可能从来都不会实际发生,或者发生的不是很频繁,以至于在测试过程中不会碰上它们。实际上,你可能无法编写出能够针对你的并发程序生成故障条件的测试代码。所产生的故障经常是偶尔发生的,并且经常是以客户端抱怨的形式出现的。这是研究并发问题的最强理由:如果视而不见,你就会遭其反噬。

    因此,并发看起来充满了危险,如果你对它有所畏惧,这可能是件好事。尽管java SE5在并发方面做出了显著的改进,但是仍旧没有像编译期验证或检查型异常这样的安全网,在你犯错误时候告知你。使用并发时,你得自食其力,并且只有变得多疑而自信,才能用java编写出可靠的多线程代码。

    有时人们会认为并发对于介绍语言的书来说太高级了,因此不适合放在其中。他们认为并发是一个独立主题,可以单独来处理,并且对于少数出现在日常的程序设计中的情况(例如图形化用户界面),可以用特殊的惯用法来处理。如果你可以回避,为什么还要介绍这么复杂的主题呢?

    唉,如果是这样就好了。遗憾的是,你无法选择何时在你的java程序中出现线程。仅仅是你自己没有启动线程并不代表你就可以回避编写使用线程的代码。例如,Web系统是最常见的java应用系统之一,而基本的Web类库、Servlet具有天生的多线程性——这很重要,因为Web服务器经常包含多个处理器,而并发是充分利用这些处理器的理想方式。即便是像Servlet这样看起来很简单的情况,你也必须理解并发问题,从而正确地使用它们。图形化用户界面也是类似的情况,你将在下一大章节中看到。尽管Swing和SWT类库都拥有针对线程安全的机制,但是不理解并发,就很难了解如何正确的使用它们。

    java是一种多线程语言,并且提出了并发问题,不管你是否意识到了。因此,有很多使用中的java程序,要么只是偶尔工作,要么在大多数时间里工作,并且会由于未发现的并发缺陷而时不时地神秘崩溃。有时这种崩溃是温和的,但有时却意味着重要数据的丢失,并且如果没有意识到并发问题,你可能会最终认为问题出在其他什么地方,而不在你的软件中。如果程序被迁移到多处理器系统中,这些种类的问题还会被暴露或放大。基本上,了解并发可以使你意识到明显正确的程序可能会展示出不正确的行为。

    学习并发编程就像进入了一个全新的领域,有点类似于学习一门新的编程语言,或者至少是学习一整套新的语言概念。要理解并发编程,其难度与理解面向对象编程差不多。如果你花点儿功夫,就能明白其基本机制,但要想真正的掌握它的实质,就需要深入的学习和理解。本章的目标就是要让读者对并发的基本知识打下坚实的基础,从而能够理解其概念并编写出合理的多线程程序。注意,你可能很容易就会变得过分自信,在你编写任何复杂程序之前,应该学习一下专门讨论这个主题的书籍。

一、并发的多面性

    并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界限)。因此,你必须理解所有这些问题和特例,以便有效地使用并发。

    用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种。

(1)更快的执行

    速度问题初听起来很简单:如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。当前,Moore定律已经有些过时了(至少对于传统芯片是这样),速度提高是以多核处理器的形式而不是更快的芯片的形式出现的。为了使程序运行得更快,你必须学习如何利用这些额外的处理器,而这正是并发赋予你的能力。

    如果你有一台多处理器的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大地提高吞吐量。这是使用强有力的多处理器Web服务器的常见情况,在为每个请求分配一个线程的程序中,它可以将大量的用户请求分不到多个CPU上。

    但是,并发通常是提高运行在单处理器上的程序的性能。

    这听起来有些违背直觉。如果你仔细考虑一下就会发现,在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当做单个任务运行好像开销更小一点,并且可以节省上下文切换的代价。

    使这个问题变得有些不同的是阻塞。如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。但是如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器上使用并发就没有任何意义。

    在单处理器系统中的性能提高的常见示例是事件驱动的编程。实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面。考虑这样一个程序,它因为将执行某些长期运行的操作,所以最终用户输入会被忽略,从而成为不可响应的程序。如果有一个“退出”按钮,那么你肯定不想在你写的每一段代码中都检查它的状态。因为这会产生非常尴尬的代码,而我们也无法保证程序员不会忘记这种检查。如果不使用并发,则产生可响应用户界面的唯一方式就是所有的任务都周期性的检查用户输入。通过创建单独的执行线程来响应用户的输入,即使这个线程在大多数时间都是阻塞的,但是程序可以保证具有一定程度的可响应性。

    程序需要连续执行它的操作,并且同时需要返回对用户界面的控制,以便使程序可以响应用户。但是传统的方法在连续执行其操作的同时,返回对程序其余部分的控制。事实上,这听起来就像是不可能之事,好像CPU必须同时位于两处一样,但是这完全是并发造成的一种错觉(在多处理器系统中,这就不只是一种幻觉了)。

    实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序),尽管这使得每个进程看起来在其执行过程中都是歇歇停停。进程总是很吸引人,因为操作系统通常会将进程互相隔离开,因此它们不会彼此干涉,这使得用进程编程相对容易一些。与此相反的是,像java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。

    每个任务都作为进程在其自己的地址空间中执行,因此任务之间根本不可能互相干涉。更重要的是,对进程来说,它们之间没有任何彼此通信的需要,因为它们都是完全独立的。操作系统会处理确保文件正确复制的所有细节,因此,不会有任何风险,你可以获得更快的程序,并且完全免费。

    有些人走的更远,提倡将进程作为唯一合理的并发方式,但遗憾的是,对进程通常会有数量和开销的限制,以避免它们在不同的并发系统之间的可应用性。

    某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常被称为函数型语言,其中每个函数调用都不会产生任何副作用(并因此而不能干涉其他函数),并因此可以当作独立的任务来驱动。Erlang就是这样的语言,它包含针对任务之间彼此通信的安全机制。如果你发现程序中某个部分必须大量使用并发,并且你在试图构建这个部分时碰到了过多的问题,那么你可以考虑使用像Erlang这类专门的并发语言来创建这个部分。

    java采取了更加传统的方式,在顺序型语言的基础上提供对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对java而言,是一个重要的设计目标。例如,在OSX之前的Macintosh操作系统版本(java第一个版本的一个非常重要的目标系统)不支持多任务,因此,除非在java中添加多线程机制,否则任何并发的java程序都无法移植到Macintosh和类似的平台之上,这样就会打破“编写一次,到处运行”的要求。

(2)改进代码设计

     在单CPU机器上使用多任务的程序在任意时刻仍旧只在执行一项工作,因此从理论上讲,肯定可以不用任何任务而编写出相同的程序。但是,并发提供了一个重要的组织结构上的好处:你的程序设计可以极大地简化。某些类型的问题,例如仿真,没有并发的支持是很难解决的。

    大多数人都看到过至少一种形式的仿真,例如计算机游戏或电影中计算机生成的动画。仿真通常涉及许多交互式元素,每一个都有“其自己的想法”。尽管你可能注意到了这一点,但是在单处理器机器上,每个仿真元素都是由这个处理器驱动执行的,从编程的角度看,模拟每个仿真元素都有其自己的处理器并且都是独立的任务,这种方式要更容易得多。

    完整的仿真可能涉及非常大量的任务,这与仿真中的每个元素都可以独立动作这一事实相对应。多线程系统对可用的线程数量的限制通常都会是一个相对较小的数字,有时就是数十或数百这样的数量级。这个数字在程序控制范围之外可能会发生变化——它可能依赖于平台,或者在java中,依赖于java版本。在java中,通常要假定你不会获得足够的线程,从而使得可以为大型仿真中的每个元素都提供一个线程。

    解决这个问题的典型方式是使用协作多线程。java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉许多,并且对可以同时执行的线程数量在理论上没有任何限制。当你处理大量的仿真元素时,这可以是一种理想的解决方案。但是注意,某些协作式系统并未设计为可以在多个处理器之间分布任务,这可能会非常受限。

    在另一个极端,当你用流行的消息系统工作时,由于消息系统涉及分布在整个网络中的多台独立的计算机,因此并发就会成为一种非常有用的模型,因为它是实际发生的模型。在这种情形中,所有的进程都彼此完全独立的运行,甚至没有任何可能去共享的资源。但是,你仍旧必须在进程间同步信息,使得这个消息系统不会丢失信息或者错误的时刻混进信息。即使你没有打算在眼前大量使用并发,理解并发也会很有用,因为你可以掌握基于消息机制的架构,这些架构在创建分布式系统时是更主要的方式。

    并发需要付出代价,包含复杂性代价,但是这些代价与在程序设计、资源负载均衡以及用户方便使用方面的改进相比,就显得微不足道了。通常,线程使你能够创建更加松散耦合的设计,否则,你的代码中各个部分都必须显式的关注那些通常可以由线程来处理的任务。

如果本文对您有很大的帮助,还请点赞关注一下。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/151210
推荐阅读
相关标签
  

闽ICP备14008679号