多核编程笔记——第九章 链表:从有锁到无锁

这篇笔记探讨了多核编程中链表操作的并发控制,从粗粒度锁到细粒度锁,再到乐观同步和无锁同步。详细分析了删除和插入操作的正确加锁策略,强调了‘hand over hand’加锁的重要性,以避免并发问题。
摘要由CSDN通过智能技术生成

前言

这一系列笔记以Maurice Herlihy等编著的The Art of Multiprocessor Programming及其PPT的practice部分为基础,主要注重于并发编程的性能。会对其做一定的扩展和省略,其中的Java代码也许会改成C++代码(因为我更熟悉C++)

粗粒度锁同步

大家都会

细粒度锁同步

删除

错误的加锁方式:单个加锁

head->n1->n2->n3->n4->n5->tail

当我们想要删除某个节点时,比如说删除n4,我们一定会做的事情是

n3->next=n5

看起来只写了n3,那么我们是不是可以有这样的加锁策略:读的过程中加锁,因此加锁顺序是head n1 n2 n3,并且拿到下一个节点的锁后释放当前锁。这样到了n3时,我们就只需要对n3加锁(前面的锁已经释放掉了),在锁的保护下改掉n3的next指针即可。

这样做的问题用下图可以清晰地表示。现在有两个线程B和C,B想要删除b,C想要删除c。如果此时C拿到了b的锁,B拿到了a的锁并且读到了b->next是c,C将b的指针重定向到d, B将a的指针重定向到c。这时候c就没有被删除了,因此出现了问题。其实问题就在于B读到c的时候没有对b加锁(因为b->next是b的成员,读取b当然是应该加锁的啦,如果假设b->next是一个原子指针,这只是避免了读写数据竞争,但并没有保证remove"事务"之间的"隔离性")。
在这里插入图片描述

正确的加锁方式:前驱锁和目标锁

由上面的讨论应该可以知道,正确的加锁方式是hand over hand的加锁:每次移动都应该持有两个锁。以下面这个链表删除n4为例,重要的步骤是

head->n1->n2->n3->n4->n5->tail
  1. 获取head锁,获取n1锁
  2. 释放head锁,获取n2锁
  3. 释放n1锁,获取n3锁
  4. 释放n2锁,获取n4锁
  5. 此时n3和n4被锁住,发现n4是目标节点,于是n3->next = n4->next

插入

插入时会在p后面插入n。实际上我们只需要对p加锁就行了。在p被加锁后,后继节点s的地址被读到(p->next),并且s不可能被删除,因为想要删除s就要拿到p的锁。同时,插入过程中n也不可能被加锁,因为p已经加锁了,其他线程无法绕过p。

实现

struct Node {
   
  int val_;
  std::mutex mtx_;
  std::shared_ptr<Node> next_;

  Node() : val_(0) {
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值