在冯诺依曼系统中,CPU和内存是核心。内存就像一个小格子,里面保存着程序要读写的值。当只有一个线程访问内存时,事情就很简单了;但是,当有多个线程时,可能会存在相互覆盖的危险:在多个线程并发执行的情况下,为了得到正确的结果,必须加Lock。看起来加锁是一件容易的事,其实不然。我们来看一个转账的例子:有两个账户,账户A和账户B,现在有一个线程1想从账户A转账50元到账户B。为了防止其他线程并发操作互相覆盖,需要加锁:貌似没有问题,但是如果有另外一个线程同时要从B账户转30元到A账户,它也使用了类似的锁定方法,有一个问题:有一个很简单的方法可以解决这个问题:按顺序锁定帐户。比如所有线程先锁定A账号,再锁定B账号,这样死锁就消除了。看到了吧,多线程并发编程稍不注意就会出错。你以为多线程编程是这样的:其实写出来的程序是这样的:而且CPU利用率很可能是“单核难,多核看”。锁这么麻烦,我们能不能不用锁?它被看作是一个简单的值,而是一个黑盒子对象:在这个黑盒子里,保存着200的账户余额。想要充值就发送充值消息,想要取款就发送取款消息,发送完消息就可以取款(异步操作)。不管是1条消息还是100条消息,都放在黑盒的一个队列中,然后Account的黑盒一条一条处理,增加或减少余额。外界看不到余额,只能通过消息与这个黑盒子进行交互。黑匣子的内部命令都处理好了,自然不用加锁了。这个黑盒子就是Actor。尝试使用Actor方法实现转账,看看和之前有什么不同:“转账Actor”收到转账50元的消息,向A账户发送提现50元的消息,向A账户发送存入50元的消息B账号,然后A账号和B账号收到消息并处理。很清楚了,根本不需要锁,每个actor都是一个独立的个体,彼此之间通过消息进行交互就可以了。但是,在转账过程中,如果其他线程也对A账户进行操作,导致A账户余额不足,抛出异常,而B账户却继续存入50元,那么转账实际上是错误的!这时候,我们想到了什么?是的,就是事务的原子性:要么不做,要么全部做。所以让这些Actor支持事务就有点麻烦了,因为Actor是独立的,消息是异步发送的,但是现在转账需要存和取两个操作需要同步进行,并且满足原子性。天下真的没有免费的午餐。这里需要解决两个问题:1.A账户和B账户中的Actor需要相互等待,直到充值和提现操作完成,如果有一个没有完成(比如抛出异常),就会被回滚。2、由于消息是异步发送的,当一个交易执行时,可能有其他的消息被放入消息队列进行处理,而账户A和账户B的余额是不断变化的,所以需要对这种变化的情况进行处理。可以参考大名鼎鼎的CAS的原理。在每次交易开始时,记录原始值并在提交时与当前值进行比较。如果相同,说明这段时间没有修改,提交成功。否则,读取当前值,重做事务中的操作:不加锁,不断尝试。由于数据是在内存中操作的,所以频繁尝试问题不大(在并发冲突不是很激烈的情况下),这种方式实现的事务被称为软件事务内存(STM)。综上所述,Actor看起来很简单,对于单个账户操作来说是一个很棒的模型,因为账户之间是相互隔离的。但是对于一致性要求比较高的场景,比如转账,Actor模型就显得有些笨拙了。【本文为专栏作家“刘欣”原创稿件,转载请通过作者微信获取授权公众号coderising】点此查看该作者更多好文
