您的当前位置:首页正文

当多线程并发遇到Actor

2024-11-11 来源:个人技术集锦

1

多线程并发的难题

张大胖在做一个银行相关的项目,写了一个Account的类,用来表示一个用户的银行账号,根据银行的常规业务,自然要提供两个方法,存款(deposit)和取款(withdraw)。

为了防止多线程并发时导致的数据不一致问题,张大胖给每个方法都加了synchronized, 那意思很清楚,想进入某个方法执行存款或取款操作,必须得先获得一把锁才行。

(注:为了简化,这里没有做边界条件检查。)

但是在做转账操作的时候,为了保证一致性,必须得把两个账户都加上锁,然后才可以操作,于是张大胖写下了这样的代码,他觉得很简单,立刻就提交给Bill ,让他Review。

富有经验的Bill立刻就发现了问题,马上对张大胖说:“这样会出现死锁!”

张大胖说:“这么简单的代码,怎么可能有死锁?”

“假设线程1 做的操作是账户A给账户B转账, 先锁住了A账户, 接下来试图申请B账户的锁;

与此同时线程2 在从 账户B给账户A 转账, 先锁住了B账户的锁, 接下来试图申请A账户的锁。

两个线程各自持有资源, 然后等待获取对方的资源, 都无法执行下去, 死锁就出现了!”

张大胖无言以对,不得不承认Bill是正确的。他问道:“那怎么解决这个问题?”

“非常简单,加锁的时候按次序来就可以了,例如所有的线程,无论是从A向B转账,还是从B向A转账,都先获得账号A的锁,成功后再获得账户B的锁,这样就没问题了。”

张大胖说:“那样代码会变得很古怪啊,还得给两个账户排个顺序,如果不知道背后的思想读起来很痛苦,怪不得人家说多线程编程很难啊。”

Bill说:“是啊, 其实线程这个东西,就是一段代码的执行而已, 是操作系统层面的概念,可是我们苦逼的程序员不得不来面对它,来背这个多线程并发的锅了。”

2

黑盒子

下班后,张大胖一直在思考这个问题:既然线程是操作系统层面的概念,能不能把线程的概念隐藏起来,然后所有的操作都不用加锁呢? 这样以来编程就会容易得多啊!

本质的问题是什么?

首先是共享的状态,例如Account中的balance ,多个线程都要读写, 其次就是多个线程乱序、并发执行。

能不能换个思路,把这个Account对象看成一个黑盒子,你想存款了,就发一个存款的消息过来,想取款就发一个取款的消息过来。

不管是有一个消息,还是有100个消息,我统统放到黑盒子的一个队例中,然后让Account对象一个个顺序处理不就可以了? 根本不用在方法上加锁!

这样做,其实就是把并发的操作变成了串行的操作而已!

不对,如果调用方把取款消息放下就走, 不等待返回结果, 那就不是同步操作,而是异步操作了!

但是如果取款的时候发现余额不足,怎么通知调用方?嗯,调用方也必须是个黑盒子对象,也向它发送异步消息,这个消息也会在消息队列中存下来,调用方“黑盒子”也会一个个处理。

想到这一层,张大胖激动起来:取款和存款的操作就不用在加锁了,码农们只要考虑黑盒子对消息的处理即可:取出消息,处理消息,向别的黑盒子发送消息,  根本不用考虑线程这样底层的概念了。

3

Actor模型

第二天张大胖赶紧找到Bill, 向他炫耀自己的“新发明”。

Bill不动声色:“小伙子,不错啊,重新发明了轮子!”

“重新发明?”

“是啊,你这个所谓黑盒子,就是所谓Actor模型啊! 它最早由Carl Hewitt在1973定义,其消息传递的方式更加符合面向对象的原始意图, 这一点我想你也体会到了,要不你怎么把他们叫做黑盒子啊。”

“1973年? 我还没出生。唉,看来这些概念已经被老前辈们都发明完了啊。”

“Actor属于并发组件模型 ,可以把程序员从多线程并发或线程池等基础概念中解放出来。它有这么几个特点:”

Actor:

就是你说的黑盒子,系统是由很多Actor组成。 Actor之间不共享状态,但是会接收别的Actor发送的异步消息,处理的过程中,会改变内部状态,也可能向别的Actor发送消息。

Message:

消息是不可变的, 它的发送都是异步的,Actor内部有个“MailBox”来缓存消息。

MailBox:

Actor内部缓存消息的邮箱, 其他Actor发送的消息都放到这里,然后被本Actor处理,类似有多个生成者和一个消费者的队例。

张大胖说:“和我之前的图差不多,看来我确实是重新发明了轮子啊。”

4

用Actor实现转账

Bill 笑道:“这个Actor看起来很美,但是编程的时候你得刷新一下你的思维才行。 大胖,之前你的转账操作在多线程下不是会出现死锁吗? 你考虑下,如果用Actor的思路该怎么写?”

“首先,得有两个Actor, 这两个Actor 表示了两个账户,我把它们叫做旺财和小强。”

“然后呢,转账的逻辑怎么处理?”

张大胖想了一会:“既然转账是在两个Actor之间发生的,那可以引入一个协调者Actor,叫做转账管家吧。不过,由于消息都是异步的,转账管家向旺财这个Actor发起扣款请求以后,不知道什么时候才能真正执行扣款,也不能立刻知道是否成功,必须得等待啊,这就有点麻烦了。”

Bill说:“我给你画个流程图,你看看。”

张大胖感慨地说:“原来的多线程并发模型,需要同时锁住两个账户,然后才能进行转账。现在每个Actor都独立,也把这个转账给搞定了。”

Bill说:“其实对于转账管家来说,对每个转账的消息,内部是隐含一个流程状态的,就是先向某个账户扣款,成功以后再向另一个账户增加,最后给调用者返回状态,这个次序是不能乱的。看到图中那个Transaction ID没有(Tx01),就是用来跟踪这个转账的事务。”

4

漏洞

“我发现了一个漏洞,你这个转账虽然看起来很美,没有加锁,但是和原来的是有区别的,原来多线程思路是会把旺财和小强的账户同时锁住,然后转账,在这个过程中,别人是不能操作这两个账号的! 而你的Actor方案中,当转账管家给旺财发消息扣款的时候,小强其实是自由的,如果这时候小强的账户被冻结,那你的转账管家还得回滚旺财的扣款,这多麻烦啊。”

Bill:“哈哈,你小子还挺机灵的嘛,看出了这个问题,Actor模型非常适用于多个组件独立工作,相互之间仅仅依靠消息传递的情况。如果想在多个组件之间维持一致的状态(比如咱们例子中的转账),那就不爽了。”

“那怎么解决这个问题?”

“那必须得用一些特殊手段了,有些实现Actor的框架,例如Akka,专门提供了像Coordinated /Transactor这样的机制来处理这个问题。有空的话给你仔细讲讲。”

“好吧,我回头看看这个Akka, 对了, Actor虽然对用户隐藏了线程, 但是总得有线程来处理消息吧。” 张大胖问道。

“那是肯定的,线程本质上就是一段代码的执行,每个Actor在处理消息的时候,肯定得和线程关联才行,只不过Actor系统把线程这个概念给隐藏了。

“有哪些系统实现了Actor?” 张大胖接着问。

“其实最著名的就是Erlang了,Actor模型可以说是它的基础,除了我们上面所说的,还可以让Actor之间建立关联,例如让一个Actor去监控另外一些Actor工作,如果那些Actor崩溃了,就新建一个Actor继续工作。在Java 领域,刚才提到的Akka是比较知名的一个Actor框架。 ”

(完)

你看到的只是冰山一角, 更多精彩文章,请移步《》或者《》


码农翻身

用故事讲述技术

显示全文