|
|
|
|
why doesn't he check again before proceeding to delete old_head? Couldn't another thread have possibly called pop() right after #3, thus incrementing the counter, and by the time the first thread reaches #4, then threads_in_pop would no longer be zero? [...] How could he safely delete old_head in that case? |
...prevent lock contention on free(). Perhaps they reasoned that if threads_in_pop gave the same value twice in a row then the application was out of a parallelism-heavy section. |
The basic problem is that you want to free a node, but you can’t do so until you’re sure there are no other threads that still hold pointers to it. If only one thread ever calls pop() on a particular stack instance, you’re home free... ...On the other hand, if you need to handle multiple threads calling pop() on the same stack instance, you need some way to track when it’s safe to delete a node. This means you need to write a special-purpose garbage collector for nodes... ...If there are no threads calling pop() , it’s perfectly safe to delete all the nodes cur- rently awaiting deletion. Therefore, if you add the nodes to a “to be deleted” list when you’ve extracted the data, then you can delete them all when there are no threads call- ing pop() . How do you know there aren’t any threads calling pop() ? Simple—count them. If you increment a counter on entry and decrement that counter on exit, it’s safe to delete the nodes from the “to be deleted” list when the counter is zero. It will have to be an atomic counter so it can safely be accessed from multiple threads... ...If the count of threads_in_pop is 1 when you’re trying to reclaim the node, you’re the only thread currently in pop() , which means it’s safe to delete the node you just removed, and it may also be safe to delete the pending nodes. If the count is not 1, it’s not safe to delete any nodes, so you have to add the node to the pending list... ...This works reasonably well in low-load situations, where there are suitable quies- cent points at which no threads are in pop() . But this is potentially a transient situa- tion, which is why you need to test that the threads_in_pop count decrements to zero d before doing the reclaim and why this test occurs before you delete the just- removed node. |
I'm confused. What is "lock contention" on free? There are no locks here since the data structure operates atomically and is lock-free, so what exactly do you mean? |
Also, you mention that the calling thread is the only thread that knows about old_head, so does that mean that old_thread could have been deleted before line 5 in the source code? What about before line 3 in the source code? |
...On the other hand, if you need to handle multiple threads calling pop() on the same stack instance, you need some way to track when it’s safe to delete a node. This means you need to write a special-purpose garbage collector for nodes... |
EITHER the list is empty, OR the current thread contains the only pointer into the removed node. If anything else happens then the CPU is not working properly. |
I don't know what you mean. |
OR the current thread contains the only pointer into the removed node. |
delete old_head;
.
One of them then does the CAS and moves on to delete the pointer (imagine there is no threads_in_pop test) Then the rest of them attempt CAS, but the pointer they hold was freed, so they segfault if lucky |
delete old_head;
if(!threads_in_pop) delete old_head;
any number of threads may execute head.load() at the same time and hold copies of the same head pointer in their local "old_head" variables. |
old_head=head.load();
inside the body of the while. Again, very weird code.
Why doesn't the author write this instead? if(!threads_in_pop) delete old_head; |
Ah, you're right. It's missing the old_head=head.load(); inside the body of the while. Again, very weird code. |
other threads may come into pop() and call head.load(), but they came in after your thread's threads_in_pop==1, therefore after your CAS, therefore the pointer they got out of head.load() is not a copy of your old_head. |
|
|
|
|
Before the function is even evaluated, we have a problem. The second parameter of the function call dereference old_head and accesses next within it in order to pass it as an argument. This could be bad if the original thread has deleted that memory. |
|
|
|
|
In order for a use-after free |