Lecture 13:Concurrency 6

本章小结:除了锁之外——并发的替代方法

锁的问题

用锁(互斥锁mutex / 信号量semaphore)做线程同步时,存在多个问题。例如:

  • 死锁(Deadlock):多个线程互相等待对方释放锁,永远阻塞。

  • 饥饿(Starvation):某些线程一直抢不到锁,长期得不到 CPU。

  • 优先级反转(Priority Inversion):高优先级线程被低优先级线程阻塞,因为后者正持有锁。

  • 缺乏组合性(Lack of Compositionality):您不能将正确的小并发程序组合成正确的大并发程序。两个各自“正确”的小并发程序,拼在一起反而出错;锁无法像函数一样自由组合。

  • 活锁(Livelock):线程发现拿不到锁就回退重试,结果大家都在重试,谁也进不了临界区。

  • 隐式共享状态:对共享状态的访问是隐式的——很难判断谁在修改什么,以及当时是否持有合适的锁。用锁时,共享变量散布各处,很难一眼看出“谁在改什么”以及“是否已加锁”。

  • 锁护送(Lock Convoy):多个线程最终在持有锁的线程后面排队,变成串行执行;一旦队首线程被调度器换出,整条“护送队”都跟着卡顿,可能需要很长时间才能清除。

综上,传统锁机制功能强,但副作用多,调试难、组合难、易死锁、易饥饿、易性能抖动。因此,我们将考虑两个备选方案。

可选的并发方法

1. 消息传递-同步或异步

同步消息传递

避免共享内存,线程/进程之间完全不共享变量,而是通过通道(channel)c,d,… 来交换数据。

通道c上有两种操作:

  • c(x)通过通道发送消息x:把值 x 放进通道 c
  • \bar{c}(x)通过通道接收到x的值:从通道 c 取出值并存入变量 x

通信是同步的——发送必须等待相应的接收,反之亦然。类比打电话(拨号方(发送)要等对方接起(接收),否则就一直等待)。发送方必须等到接收方准备好(反之亦然),双方同时在线才能完成一次传输;

  • Thread 1 执行 c(data)发送 数据,但 Thread 2 尚未准备好接收,于是 Thread 1 阻塞

  • Thread 2 随后执行 \bar{c}(x)接收,此时双方都已就位,于是: – 数据 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需要两个步骤:

  1. 从Dan的账户中扣除10英镑。
  2. 在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;
  1. begin transaction; —— 事务开始。

  2. 两条 update —— 把 Geert 的钱增加、Dan 的钱减少;两条语句之间若发生故障,数据库仍处于“未提交”状态。

  3. commit —— 如果一切正常,一次性把两条更新永久写入数据库。

  4. rollback —— 若中途出现任何异常(余额不足、网络中断、约束冲突等),立刻撤销已做过的所有更改,保持“什么都没发生”的原子性。

要么两个更新都发生,要么两个都不发生:要么全部成功(commit),要么全部撤销(rollback),从而保证数据始终一致,不会出现“转了一半”的中间状态。

Rollback:

现实的交易可能更复杂,并且可能遇到无法成功的原因。它们显式地调用rollback 以使整个事务失败,而不是commit

线程协调事务

事务性内存的概念是让线程之间共享的内存以事务性的方式运行,就像数据库一样。

do {
  begin_transaction();
  modify_shared_data();
  commit();
} while(!transaction_succeeds());
  • 没有锁,也没有死锁的风险
  • 一种形式的活锁仍然是可能的——因为我们在回滚(rollbacks)后后退并再次尝试。
  • 饥饿仍然是可能的——较小的事务可能会导致较长的事务重复回滚。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值