Why is the following snippet for deleting a node in a linked list not thread safe?
edit: note every node has a lock of its own
// ... lock acquisitio
I assume that you are talking about a singly linked list, since you never assign 'prev' in your node-deletion. Given a singly linked list of nodes, each protected by a lock, it might be depicted as follows:
Head ==> A ==> B ==> C ==> D ==> Tail
^ ^
| |
Thread 1 Thread 2
Let's say that Thread 1 is going to delete node B. Coincidentally, Thread 2 is going to try to delete node C at the same time. The steps that you give might execute as follows:
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
In this case, the result is unpredictable. If Thread 2 executed slightly ahead of Thread 1, then everything should be fine. Thread 1's second line would perform "A->next = D" since Thread 2 would have already changed B->next to D. However, if Thread 1 executes slightly ahead of Thread 2, then A->next points to dead node C, dead node B was modified, and node D is lost.
So, you might try to lock the node you're going to delete, then lock 'prev' before modifying it. The steps might execute as follows:
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
So, this still isn't thread-safe. A->next points to dead node C, dead node B was locked and used, and D is lost. All we've done is make sure that the error case above happens reliably.
The solution here would seem to require a lock on 'prev' before locking the node to be deleted.
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->next points to D, and both B and C are now deleted.
It is thread safe assuming the scope of your lock (meaning what it locks, nothing to do with the official term "scope" used in C) is large enough.
If it locks just the current node p
, then you can't rely on other threads not coming in and playing with prev
(or head
or tail
for that matter) and hence under-cutting you.
If it locks the entire structure, then yes, it is thread-safe.
We can't tell the scope of your lock from the code given but I will mention one other (unrelated) thing.
You should probably either free p
or add it to a free list for re-use. Simply setting its next
pointer to null and its deleted
flag to 1 won't let you find it when you need to reuse it. That will lead to a memory leak. It may be that the code to do this just isn't shown but I thought I'd mention it, just in case.
Based on your edit where you state you're using a fine-grained approach (one lock per node):
Provided you lock all three of the "nodes" that you're using or changing, and that you lock them in a consistent direction, it's still thread-safe.
I put "nodes" in quotes since it also applies to the head
and tail
pointers. For example, if you want to delete the first node in a ten-node list, you need to lock the head
variable and the first and second nodes, in that order. To delete the last node in a one-node list, you need to lock both the head
and tail
variables and the node.
Locking of all three "nodes" will prevent threads from adversely affecting each other.
Locking them in a consistent direction (such as from head
towards tail
) will prevent deadlocks.
But you have to lock all three before attempting to change anything.
This will even prevent it from concurrent insert operations provided the insert locks the two "nodes" on either side of the insertion point and, of course, locks them in the same direction.
Not sure how well iterating over the list would go though. You could probably get away with a system whereby you initially lock the head
variable and the first node, then release head
.
Then, when you've finished with that node, lock the next one before releasing the current one. That way, you should be able to iterate through the list without being affected by inserts or deletes, which can only happen in areas you're not currently working with.
But, the bottom line is that you can certainly make it thread safe, even with a fine-grained locking scope.
You might wanna take a look at this presentation. From slide #39, it demonstrates how fine-grained linked list locking should be implemented, in a clear and figurative way (the slides' notes add some explanations as well). The presentation is based on (or taken from...) a book called The Art of Multiprocessor Programming.