
1.1 并发编程
在并发编程中,程序被描述为一些并发计算的集合,这些计算在时间上重叠,在执行过程中互相协调。实现正确运行的并发程序比实现串行程序困难多了。串行程序中的所有陷阱在并发程序中同样存在,而并发程序本身还有很多问题。于是有人会说:“何必呢?继续写串行程序不好吗?”
这是因为并发编程的好处还是很多的。首先,提高并发度可以改进程序性能。在一个处理器上运行整个程序会很慢,让不同的处理器同时运行多个子任务可以提高速度。随着多核处理器的流行,性能因素成为并发编程获得关注的主要原因。
其次,并发编程模型可以实现更快的I/O操作。纯串行程序必须周期性地查看I/O,以检测是否有来自键盘、网卡或其他设备的数据输入。而并发程序可以立即响应 I/O 请求。对于 I/O 密集型操作,这会提高数据吞吐量,这也是在多核处理器出现之前不少程序语言中就已经支持并发编程的原因。因而,并发性可保证提高程序与环境交互时的响应性(responsiveness)。
最后,并发性可简化计算机程序的实现和提高可维护性。有些应用程序用并发性来表述时会更为简洁。与其将所有计算过程嵌入一个较大的工程中,将它分到更小且独立的计算过程中可能会更方便一些。用户接口、网络服务器以及游戏引擎都属于这类应用。
本书假设并发程序通过共享内存的方式互相通信,且所有程序都在同一台计算机上运行。相比而言,分散在不同计算机上,且各自拥有独立内存的程序,称为分布式程序。编写分布式程序的领域称为分布式编程。一般而言,分布式程序假设每台计算机都可能失效,因而提供了应对这种情况的保护措施。本书重点关注并发程序,但也会涉及一些分布式程序。
1.1.1 传统并发计算概述
在一个计算机系统中,并发性具有多个层次,它可以存在于计算机硬件层面、操作系统层面,也可以存在于编程语言层面。本书主要关注编程语言层面的并发性。
在并发计算系统中,多个执行(execution)之间的协调称为同步,这是实现并发性的关键要素。同步涉及维持并发程序时序性的机制。此外,同步指定了并发执行之间的通信方式,即如何交互信息。在并发程序中,不同的执行通过修改计算机的共享内存子系统实现通信。这种通信称为共享内存通信。在分布式程序中,不同执行之间通过交换消息实现通信,这种通信称为消息传递通信。
在底层,并发执行称为进程和线程的实体表示,详情参见第2章。进程和线程使用一些传统实体(比如锁和监控器)来维护相互之间的运行次序。在线程之间建立运行次序保证了前一个线程对内存的修改在后一个线程中是可见的。
一般而言,单独用线程和锁来表达并发程序是很烦琐的。于是,产生了一些更复杂的并发工具用于解决这个问题,比如通信通道、并发容器、同步栅栏(barrier)、计数闩(countdown latch)、线程池等。这些工具可以更好地表达特定类型的并发编程模式,第3章会介绍其中一部分。
相比而言,传统的并发编程更底层一些,且容易出错,比如死锁、饥饿、数据争用和竞态条件(race condition)。使用Scala编写并发程序时,一般不会使用底层并发原语。不过,对底层并发编程有基本了解也是有价值的,这对进一步理解高层次的并发概念很有帮助。
1.1.2 现代并发编程范式
现代并发编程范式比传统方法更高级,关键的区别在于高层次的并发框架更关心如何表述目标,而不是如何实现目标。
在实践中,底层和高层并发之间没那么泾渭分明,而且一些并发框架进一步填充了两者之间的空白,形成了一个并发框架的谱系。不过,并发编程目前的发展趋势更倾向于声明式和函数式风格。
在第2章中会看到,并发计算一个值需要用到一个线程,该线程要定制其run方法,并用start方法启动,启动之后需要等待线程结束,然后到指定内存区域读取最后的结果。总而言之,并发计算过程就是并发地计算,然后等待结束通知。
但更好的并行计算方案是选择一种编程模型,将并发计算时的通信细节隐藏。这样,用户不会感觉到自己在等待,也不需要手动去内存中取结果,仿佛计算结果自然而然就产生了。异步编程中的一种称为Future的范式特别适用于这种声明式并发计算场合,这在第4章中会介绍。类似地,响应式编程(reactive programming)使用事件流,它以一种声明式的方式表达多个值的并发计算,详情参见第6章。
声明式编程风格在串行编程中也越来越普遍。像Python、Haskell、Ruby和Scala这样的语言使用函数式操作处理容器数据结构,并支持“filter all negative integers from this collection”这样的声明式语句。这种语句表述的是目标,而不是底层的实现方式,因此后台也就有了很大的并行优化空间。第5章将描述 Scala 中的数据并行容器(parallel collection)框架,用于在多核环境下对容器操作加速。
另一个高层次并发框架的发展趋势是专用化。软件事务性内存技术专门用于表达内存事务,却丝毫不关心如何启动并发执行。内存事务是指一连串内存操作,这些操作要么都执行,要么都不执行,这个概念类似于数据库事务。使用内存事务的好处是,避免了底层并发计算的常见错误。第7章详细解释了事务性内存。
一些高层次并发框架还致力于实现分布式计算的透明化。对数据并行框架和消息传递并发框架而言尤其如此,比如第8章中的角色模型(actor model)。