本章小结:除了锁之外——并发的替代方法
锁的问题
用锁(互斥锁mutex / 信号量semaphore)做线程同步时,存在多个问题。例如:
-
死锁(Deadlock):多个线程互相等待对方释放锁,永远阻塞。
-
饥饿(Starvation):某些线程一直抢不到锁,长期得不到 CPU。
-
优先级反转(Priority Inversion):高优先级线程被低优先级线程阻塞,因为后者正持有锁。
-
缺乏组合性(Lack of Compositionality):您不能将正确的小并发程序组合成正确的大并发程序。两个各自“正确”的小并发程序,拼在一起反而出错;锁无法像函数一样自由组合。
-
活锁(Livelock):线程发现拿不到锁就回退重试,结果大家都在重试,谁也进不了临界区。
-
隐式共享状态:对共享状态的访问是隐式的——很难判断谁在修改什么,以及当时是否持有合适的锁。用锁时,共享变量散布各处,很难一眼看出“谁在改什么”以及“是否已加锁”。
-
锁护送(Lock Convoy):多个线程最终在持有锁的线程后面排队,变成串行执行;一旦队首线程被调度器换出,整条“护送队”都跟着卡顿,可能需要很长时间才能清除。
综上,传统锁机制功能强,但副作用多,调试难、组合难、易死锁、易饥饿、易性能抖动。因此,我们将考虑两个备选方案。
可选的并发方法
1. 消息传递-同步或异步
同步消息传递
避免共享内存,线程/进程之间完全不共享变量,而是通过通道(channel)c,d,… 来交换数据。
通道c上有两种操作:
- c(x)通过通道发送消息x:把值
x放进通道c; 通过通道接收到x的值:从通道
c取出值并存入变量x。
通信是同步的——发送必须等待相应的接收,反之亦然。类比打电话(拨号方(发送)要等对方接起(接收),否则就一直等待)。发送方必须等到接收方准备好(反之亦然),双方同时在线才能完成一次传输;

-
Thread 1 执行
c(data)想 发送 数据,但 Thread 2 尚未准备好接收,于是 Thread 1 阻塞。 -
Thread 2 随后执行
想 接收,此时双方都已就位,于是: – 数据
data瞬间从 Thread 1 拷贝到 Thread 2 的x;
– 两条语句 同时完成;
– 两个线程都 解除阻塞,继续往下执行。
同步消息传递的优缺点:
- 数据流是显式的-不暴露于竞态和其他困难的调试。
- 同步发送方和接收方可能会降低性能。
- 可以使用同步消息传递实现互斥锁和信号量-死锁,饥饿等都是可能的。
异步消息传递
为了减轻同步消息传递的问题,发送方不会阻塞等待可用的接收方。类比发电子邮件 (发完就去做别的事,邮件在收件箱里排队)。发送方 ! 完消息就继续往下跑,不阻塞;消息先进入接收方的队列,等对方有空时再处理。
通常使用所谓的基于Actor的模型,例如,指定一个简单的计数器:
class Counter extends Actor {
var counter = 0
def receive = {
case Zero => counter = 0
case Inc => counter = counter + 1
}
}
- Actor = 状态 + 行为(receive)。
- 状态是私有的(
var counter = 0),外部只能通过消息修改。 receive用模式匹配处理不同消息:Zero清零、Inc加 1。
然后增加一个Counter对象,你只需要给它发送一条消息:
mycounter ! Inc // Send Inc message - don’t wait for receiver
mycounter ! Inc // Send another - queued up until dealt with
Counter 会在自己的线程/调度器里依次处理这两条消息,最终 counter = 2。
异步消息传递的优缺点:
- 与同步模型一样,数据流是显式的-较少暴露于竞争条件和其他困难的调试。
- 发送方和接收方之间的耦合不太显式。
- 可能比共享内存效率低。
- 可以模拟同步消息传递。
- 可以使用异步消息传递实现互斥锁-死锁,饥饿等都是可能的。
2. 事务内存(Transactional memory)
数据库事务
数据库的类比:
假设我欠Geert 10英镑。
在银行内部,我们的账户余额可能存储在一个数据库表中:

把钱转到Geert需要两个步骤:
- 从Dan的账户中扣除10英镑。
- 在Geert账户上加10英镑。
银行转账要求:
- 我们希望转移是原子的-特别是它要么完全成功,要么完全失败。
- 如果对数据库进行了其他(原子)更改,我们希望它们是可串行化(serializable)的——就好像它们在时间上不重叠一样。
为了满足我们的需求,我们将更改封装在事务(transaction)中:
begin transaction;
update accounts set balance = balance + 10 where holder = ’Geert’;
update accounts set balance = balance - 10 where holder = ’Dan’;
commit;
-
begin transaction;—— 事务开始。 -
两条
update—— 把 Geert 的钱增加、Dan 的钱减少;两条语句之间若发生故障,数据库仍处于“未提交”状态。 -
commit—— 如果一切正常,一次性把两条更新永久写入数据库。 -
rollback—— 若中途出现任何异常(余额不足、网络中断、约束冲突等),立刻撤销已做过的所有更改,保持“什么都没发生”的原子性。
要么两个更新都发生,要么两个都不发生:要么全部成功(commit),要么全部撤销(rollback),从而保证数据始终一致,不会出现“转了一半”的中间状态。
Rollback:
现实的交易可能更复杂,并且可能遇到无法成功的原因。它们显式地调用
rollback以使整个事务失败,而不是commit。
线程协调事务
事务性内存的概念是让线程之间共享的内存以事务性的方式运行,就像数据库一样。
do {
begin_transaction();
modify_shared_data();
commit();
} while(!transaction_succeeds());
- 没有锁,也没有死锁的风险。
- 一种形式的活锁仍然是可能的——因为我们在回滚(rollbacks)后后退并再次尝试。
- 饥饿仍然是可能的——较小的事务可能会导致较长的事务重复回滚。
897

被折叠的 条评论
为什么被折叠?



