极客时间21天打卡活动:2023.1.16-2.5
链表的接口:
- 插入元素
- 删除元素
- 读取元素
并发化改造:
- 并发插入元素
- 并发删除元素
- 并发读取元素
锁,每个节点都定义一把锁。
并发插入
区域猜想:如果某个CPU 锁定了某个节点,它就控制这个节点及其后继指针这段区域。此时只有此CPU 有权限在区域内进行写操作。
-
往后如果要更改一个节点的后继指针,则必须先锁定这个节点(因为后继指针实际储存于这个节点中,此时每个节点带有一个Mutex锁)
只要我们能将写操作限制在部分区域,那么我们只要在区域中兼容一写多读的行为,整个数据结构就可以实现全局并发读写,并且读操作无锁。
两段链表的唯一联系是某个节点的后继指针。
并发插入的步骤:
最终正确步骤:
Insert:
找到节点A 和B,不存在则直接返回
锁定节点A ,检查A.next == B,如果为假,则解锁A 然后返回step 1
创建新节点X
X.next = B; A.next = X
解锁节点A
并发插入 正确性验证的用例:
情况一:多个插入操作在Step [2,5] 之间,因为都需要争抢A 节点的锁,所以不会冲突
情况二:两个插入的B1 和A2 重合,由于有序链表的结构特点,不会冲突
情况三:多个插入操作在Step [1,2] 之间,由于我们锁定A 节点后进行检查,所以不会冲突
并发删除
由于涉及2个节点的修改,需要依次锁定2个节点。
用例1:CPU0优先拿到A2的锁。
情况1并发错误的结果: B2 节点没有被正确删除!
原因:B1(A2) 实际是一个已经被删除的节点,G2 真正的A 节点实际应当是A1 而不是B1(A2),但是CPU1删除操作没有意识到这一点。
解决方案:如果一个节点被删除,则需要标记其为被删除状态(即所谓的逻辑删除),并且在删除操作的流程中,如果发现某个节点被删除,返回重新操作。
并发删除的步骤:
最终正确步骤:
Delete :
找到节点A 和B,不存在则直接返回
锁定节点B,检查b.marked == true,如果为真,则解锁B 然后返回step 1
锁定节点A,检查A.next != B OR a.marked,如果为真,则解锁A 和B 然后返回step 1
b.marked = true;A.next = B.next
解锁节点A 和B
删除操作之所以没有将A 节点的后置节点置空,是为了避免读操作无法正确访问后继节点。
并发删除 正确性验证的用例:
情况一:多个删除前后执行,由于删除标记的存在,所以都可以正常删除
情况二:插入或删除操作发生在Step [1,2] 之间,由于我们锁定A、B 节点后进行检查,所以不会冲突
情况三:插入或删除操作发生在Step [2,5] 之间,由于我们同时锁定A 和B,A.next 和B.next 都不会改变
并发读
将写操作限制在某个区域,则可实现区域内一写多读,全局多写多读。
读操作完全无锁,使用atomic 可以无限制访问节点。
参考
- https://time.geekbang.org/qconplus/detail/100073196