为什么删除链表中的节点的以下代码段不是线程安全的?
编辑:注意每个节点都有自己的锁
// ... lock acquisition here
// ... assumption found to be valid here
prev->next = p->next;
p->next = NULL;
p->deleted = 1;
答案 0 :(得分:8)
我假设您正在谈论单链表,因为您从未在节点删除中指定'prev'。给定一个单独链接的节点列表,每个节点都由一个锁保护,它可能描述如下:
Head ==> A ==> B ==> C ==> D ==> Tail
^ ^
| |
Thread 1 Thread 2
假设线程1将删除节点B.巧合的是,线程2将尝试同时删除节点C.您提供的步骤可能如下执行:
Thread 1 Thread 2
---------------------- ----------------------
Lock B Lock C
A->next = C or D; <=?? B->next = D; <== B could be dead already
B->next = NULL; C->next = NULL;
B->deleted = 1; C->deleted = 1;
Unlock B Unlock C
在这种情况下,结果是不可预测的。如果线程2稍微超过线程1执行,那么一切都应该没问题。线程1的第二行将执行“A-> next = D”,因为线程2已经改变了D-旁边的B->然而,如果线程1稍微超过线程2,则A->接下来指向死节点C,死节点B被修改,节点D丢失。
因此,您可能会尝试锁定要删除的节点,然后在修改之前锁定“prev”。步骤可以执行如下:
Thread 1 Thread 2
---------------------- ----------------------
Lock B Lock C
Lock A waiting for B
A->next = C; waiting for B
Unlock A waiting for B
B->next = NULL; waiting for B
B->deleted = 1; waiting for B
Unlock B Lock B <= locking dead node
B->next = D; <= assigning to dead node
Unlock B
C->next = NULL;
C->deleted = 1;
Unlock C
所以,这仍然不是线程安全的。 A->接下来指向死节点C,死节点B被锁定并使用,并且D丢失。我们所做的就是确保上述错误情况可靠地发生。
此处的解决方案似乎需要在锁定要删除的节点之前锁定'prev'。
Thread 1 Thread 2
---------------------- ----------------------
Lock A Lock B
waiting for B Lock C
waiting for B B->next = D;
Lock B Unlock B
A->next = D; C->next = NULL;
Unlock A C->deleted = 1;
B->next = NULL; Unlock C
B->deleted = 1;
Unlock B
A->接下来指向D,现在B和C都被删除。
答案 1 :(得分:7)
你可能想看看this presentation。从幻灯片#39开始,它以清晰和具象的方式演示了如何实现细粒度的链表锁定(幻灯片的注释也添加了一些解释)。该演示文稿基于(或取自......)一本名为The Art of Multiprocessor Programming的书。
答案 2 :(得分:5)
线程是安全的,假设你的锁的范围(意思是它锁定的,与C中使用的官方术语“范围”无关)足够大
如果它只锁定当前节点p
,那么您就不能依赖其他未进入并使用prev
(或head
或tail
的线程。这件事,因此削弱了你。
如果它锁定整个结构,那么是的,它是线程安全的。
我们无法从给出的代码中告诉您锁定的范围,但我会提到另一个(无关)的事情。
您可能应该免费p
或将其添加到免费列表中以供重复使用。只需将其next
指针设置为null并将其deleted
标志设置为1,就无法在需要重用它时找到它。这将导致内存泄漏。可能是这样做的代码没有显示,但我想我会提到它,以防万一。
根据您的编辑,您声明您正在使用细粒度方法(每个节点一个锁):
如果您锁定了正在使用或更改的所有三个“节点”,并且您将它们锁定在一致的方向,它仍然是线程安全的。
我将“节点”放在引号中,因为它也适用于head
和tail
指针。例如,如果要删除十节点列表中的第一个节点,则需要按顺序锁定head
变量以及第一个和第二个节点。要删除单节点列表中的最后一个节点,您需要锁定head
和tail
变量以及节点。
锁定所有三个“节点”将阻止线程相互产生负面影响。
将它们锁定在一致的方向(例如从head
朝tail
),可以防止死锁。
但是在尝试改变任何东西之前你必须锁定所有三个。
如果插入锁定插入点两侧的两个“节点”,当然会将它们锁定在同一个方向上,这甚至会阻止它进行并发插入操作。
不确定在列表上迭代的程度如何。你可能可以使用一个系统,你最初锁定head
变量和第一个节点,然后释放head
。
然后,当您完成该节点后,在释放当前节点之前锁定下一个节点。这样,您应该能够在不受插入或删除影响的情况下遍历列表,这只能在您当前未使用的区域中进行。
但是,最重要的是,即使使用细粒度锁定范围,您也可以确保线程安全。