使用细粒度方法线程安全地删除链表节点

时间:2011-02-23 07:38:23

标签: multithreading parallel-processing linked-list

为什么删除链表中的节点的以下代码段不是线程安全的?

编辑:注意每个节点都有自己的锁

// ... lock acquisition here
// ... assumption found to be valid here
prev->next = p->next;
p->next = NULL;
p->deleted = 1;

3 个答案:

答案 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(或headtail的线程。这件事,因此削弱了你。

如果它锁定整个结构,那么是的,它是线程安全的。

我们无法从给出的代码中告诉您锁定的范围,但我会提到另一个(无关)的事情。

您可能应该免费p或将其添加到免费列表中以供重复使用。只需将其next指针设置为null并将其deleted标志设置为1,就无法在需要重用它时找到它。这将导致内存泄漏。可能是这样做的代码没有显示,但我想我会提到它,以防万一。


根据您的编辑,您声明您正在使用细粒度方法(每个节点一个锁):

如果您锁定了正在使用或更改的所有三个“节点”,并且您将它们锁定在一致的方向,它仍然是线程安全的。

我将“节点”放在引号中,因为它也适用于headtail指针。例如,如果要删除十节点列表中的第一个节点,则需要按顺序锁定head变量以及第一个和第二个节点。要删除单节点列表中的最后一个节点,您需要锁定headtail变量以及节点。

锁定所有三个“节点”将阻止线程相互产生负面影响。

将它们锁定在一致的方向(例如从headtail),可以防止死锁。

但是在尝试改变任何东西之前你必须锁定所有三个。

如果插入锁定插入点两侧的两个“节点”,当然会将它们锁定在同一个方向上,这甚至会阻止它进行并发插入操作。

不确定在列表上迭代的程度如何。你可能可以使用一个系统,你最初锁定head变量和第一个节点,然后释放head

然后,当您完成该节点后,在释放当前节点之前锁定下一个节点。这样,您应该能够在不受插入或删除影响的情况下遍历列表,这只能在您当前未使用的区域中进行。


但是,最重要的是,即使使用细粒度锁定范围,您也可以确保线程安全。