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

Fel*_*zar 3 parallel-processing multithreading linked-list

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

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

// ... lock acquisition here
// ... assumption found to be valid here
prev->next = p->next;
p->next = NULL;
p->deleted = 1;
Run Code Online (Sandbox Code Playgroud)

Jef*_*eff 9

我假设您正在谈论单链表,因为您从未在节点删除中指定'prev'.给定一个单独链接的节点列表,每个节点都由一个锁保护,它可能描述如下:

Head ==> A ==> B ==> C ==> D ==> Tail
               ^     ^
               |     |
        Thread 1     Thread 2
Run Code Online (Sandbox Code Playgroud)

假设线程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
Run Code Online (Sandbox Code Playgroud)

在这种情况下,结果是不可预测的.如果线程2稍微超过线程1执行,那么一切都应该没问题.线程1的第二行将执行"A-> next = D",因为线程2已经改变了D-旁边的B->但是,如果线程1稍微超前于线程2,那么A-> next指向死节点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
Run Code Online (Sandbox Code Playgroud)

所以,这仍然不是线程安全的.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                
Run Code Online (Sandbox Code Playgroud)

A->下一个指向D,现在删除B和C.


era*_*ran 7

你可能想看看这个演示文稿.从幻灯片#39开始,它以清晰和具象的方式演示了如何实现细粒度的链表锁定(幻灯片的注释也添加了一些解释).该演示文稿基于(或取自......)一本名为"多处理器编程艺术"的书.


pax*_*blo 5

线程安全的假设你锁定的范围(这意味着什么锁定,没什么可在C中使用的官方术语"范围"做)是足够大的.

如果它只锁定当前节点p,那么你不能依赖其他线程不进入和prev/ head或(或者tail就此而言)并因此削弱你.

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

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

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


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

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

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

锁定所有三个"节点"将防止线程相互产生不利影响.

将它们锁定在一致的方向(例如head朝向tail)将防止死锁.

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

这甚至会阻止它进行并发插入操作,只要插入锁定插入点两侧的两个"节点",当然,它们将它们锁定在同一方向上.

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

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


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