Scala语言有很多优点,比如简洁、强大的表现力等等。但我最关注的,是它在并发编程方面的优势。
Scala通过强调对象的不变性 以及使用基于事件的模型进行线程间通信 使得实现并发应用变得简单。
并发编程之所以这么困难,很大一个原因在于对象的可变性。要在充斥着大量可变对象的程序里面实现安全并发,需要非常繁琐以及复杂易错的同步操作来保证状态更新的同步。
比如下面这段代码(java的例子),可能你会认为它已经是线程安全的了,因为所有的方法都已经被同步。
但是问题来了,当顺序调用这两个方法的时候,比如:
这时并不是线程安全的,在第一个方法调用结束之后,可能会被其它线程获取对象的锁,修改account的balance。
在命令式编程语言里面,命令查询分离是一个普遍使用的原则。意即:一个方法要么进行一次命令(执行一个操作,通常会修改状态),要么进行一次查询(返回一些状态, 但并不修改状态),而不应该同时执行命令以及查询。从面向对象的角度看,这是一个良好的设计;但从并发编程的角度看,它却带来了一些问题,使得并发编程实现更加困难。上面就是一个很好的例子。
要使得上面这段代码变得线程安全,可以通过破坏命令查询分离原则来实现:
从上面的例子看到,在可变对象的环境中实现并发编程是困难的。而不变对象,意即线程安全的对象,使得我们不必担心共享对象会被多个线程同时修改而无法保持它的正确性:因为它本身就不可修改。我们可以把不变对象很放心地扔到多线程环境中,任意使用。
问题又来了,如果要“修改状态”怎么办?很简单,创建一个新的对象。还是上面那个例子,我们把它修改成一个Scala的不变对象类。并在伴生对象里面实现increment方法,此方法会返回一个新的account对象,balance值为原有对象的值加1所得。
通过强调不变对象的使用,并发编程变得简单了很多。
传统的并发是通过线程(thread)来实现的。在传统的并发模型中,程序被分成若干份同时执行的任务,并且所有任务都对一块共享的内存进行操作。在传统的并发模型会引起竞争问题,可以采取锁机制避免竞争问题,但同时这可能带来死锁等问题。
Actor模型是另一种不同的并发模型,它很好地解决了在传统并发模型中竞争和死锁等问题。我们可以把一个由actor模型实现的并发程序看成是一个星系一样,星系里面有很多星球,每个星球都是一个actor,星球之间不共享任何资源,但是它们之间有通道来相互传递信息。
每个星球(actor)都有一个信箱来接受来自其它星球的任意信息,它会按照信息接收的顺序来处理,处理完一个信息然后接着处理下一个信息。可以按照信息类型来触发不同的行为。
同时,每个星球(actor)可以异步地(也可以同步,但不是这里谈论的重点)向其它任意星球发送任意消息,就是说,它发送消息之后不会等待返回信息而是直接执行接下来的操作。
下面是一个Actor的例子:
我们可以看到,程序里面定义两种不同的消息类型:Increment和Balance。Account是一个Actor,它跟外界的交互都是通过消息传递来实现:不论是increment,还是获取balance都是通过消息的方式来实现。当接受到不同的消息时,它会执行不同的行为。
我们也可以看到,Account的内部状态完全是自己控制的,接收到的消息是顺序执行的,所以我们不需要担心竞争问题。
Scala就是这样,通过“使用基于事件的模型进行线程间通信”,并且“把不变对象作为消息进行传递”来实现一个并发编程模型。
--EOF--