The lock-free ordered list guarantees the uniqueness of the element, making it available to the bucket of the hash table, or even directly as a less efficient map. The lock-free implementation of the normal list is relatively simple because the insertion element can be inserted in the header, and the insertion of the ordered list is arbitrary.
This paper is based on the thesis high performance Dynamic Lock-free Hash tables implementation.
Problems
The main operation of the list contains insert
and remove
, first a simple implementation of a version, you will see the problem, the following code is used only as an example:
struct node_t {key_t key; value_t Val; node_t *next; }; int L_find (node_t **pred_ptr, node_t **item_ptr, node_t *head, key_t key) {node_t *pred = head; node_t *item = head->next; while (item) {int d = key_cmp (Item->key, KEY); if (d >= 0) {*pred_ptr = pred; *item_ptr = Item; return d = = 0? True:false; } pred = Item; item = item->next; } *pred_ptr = pred; *item_ptr = NULL; return FALSE; } int L_insert (node_t *head, key_t key, value_t val) {node_t *pred, *item, *new_item; while (TRUE) {if (L_find (&pred, &item, Head, key)) {return FALSE; } New_item = (node_t*) malloc (sizeof (node_t)); New_item->key = key; New_item->val = val; New_item->next = Item; A. If the pred itself is removed if (CAS (&pred->next, item, New_item)) {return TRUE; } free (New_item); }} int L_remove (node_t *head, key_t key) {node_t *pred, *item; while (true) {if (!l_find (&pred, &item, Head, key)) {return TRUE; }//B. If pred is removed, if item is also removed if (CAS (&pred->next, item, Item->next)) {Haz_ Free (item); return TRUE; } } }
l_find
The function returns the pre-order element and the element itself, code A and B, although pred
it gets the and item
, but at CAS
the time, it may be removed by other threads. Even in the l_find
process, each of its elements may be removed. The problem is that any time you get an element, you're not sure if it's still working . The validity of an element includes whether it is still in the linked list, and whether the memory it points to is still valid.
Solution Solutions
The problem of element validity determination can be solved by adding a validity flag bit to the element pointer and cooperating with the mutex of CAS operation .
Because node_t
put in memory will be aligned, so pointing node_t
to the pointer value is not used, which can be set in the low number of flags, so in the time of the CAs, the implementation of the DCAs effect, equivalent to two logical operations into an atomic operation. Imagine the thread safety of the reference count object, where the wrapped pointer is thread-safe, but the object itself is not.
The mutex of CAs, when several threads CAs the same object, only one thread succeeds, and the failed thread can determine that the target object has changed. Improved code (code is only used for examples, not guaranteed correctly):
typedef size_t MARKABLE_T; The lowest position 1, indicating that the element was deleted #define HAS_MARK (P) ((markable_t) P & 0x01) #define MARK (P) ((markable_t) p | 0x01) #define ST Rip_mark (P) ((markable_t) P & ~0x01) int L_insert (node_t *head, key_t key, value_t val) {node_t *pred, *item , *new_item; while (TRUE) {if (L_find (&pred, &item, Head, key)) {return FALSE; } New_item = (node_t*) malloc (sizeof (node_t)); New_item->key = key; New_item->val = val; New_item->next = Item; A. Although find has a valid pred, pred may be deleted before the following code, Pred->next is marked//pred->next! = Item, the CAS will fail and retry after failure if (CAS (&pred->next, item, New_item)) {return TRUE; } free (New_item); } return FALSE; } int L_remove (node_t *head, key_t key) {node_t *pred, *item; while (TRUE) {if (!l_find (&pred, &item, Hea(d, key)) {return FALSE; } node_t *inext = item->next; B. Mark Item->next before deleting item, if the CAS fails, then the same as insert, there are other threads after the Find//deleted item, failed to retry if (!). CAS (&item->next, Inext, MARK (Inext))) {continue; }//C. On the same element when the item is deleted, only one thread will successfully go here if (CAS (&pred->next, Item, Strip_mark (Item->next))) { Haz_defer_free (item); return TRUE; }} return FALSE; } int L_find (node_t **pred_ptr, node_t **item_ptr, node_t *head, key_t key) {node_t *pred = head; node_t *item = head->next; hazard_t *hp1 = haz_get (0); hazard_t *HP2 = haz_get (1); while (item) {haz_set_ptr (HP1, pred); Haz_set_ptr (HP2, item); /* If it is already marked, then the item may be removed from the list or even released, so it needs to be looked up */if (Has_mark (Item->next)) { Return L_find (pred_ptr, Item_ptr, head, key); } int d = key_cmp (Item->key, KEY); if (d >= 0) {*pred_ptr = pred; *item_ptr = Item; return d = = 0? True:false; } pred = Item; item = item->next; } *pred_ptr = pred; *item_ptr = NULL; return FALSE; }
Functions such as
haz_get
, haz_set_ptr
are a hazard pointer implementation that supports GC for memory under multiple threads. In the preceding code, when you delete an element item
, item->next
is marked, which makes the CAS
in insert
There is no need to make any adjustments. Summarize the thread race situation here:
insert
In the find
normal and, and pred
item
pred->next == item
then in the CAS
previous thread removed pred
, at this time pred->next == MARK(item)
, CAS
failed, retry; the deletion is divided into 2 cases: a) removed from the list, tagged, pred
can continue to access; b) pred
Memory may be freed and then used pred
incorrectly. In order to deal with the situation B, so the introduction of similar hazard pointer mechanism, can effectively protect any one pointer p
as long as the thread is using it, its memory will not be really released
insert
There are multiple threads in the pred
post-insertion element, and at this point the same is guaranteed by the insert
medium, which CAS
does not say much
remove
In the case of the same insert
, find
got the valid pred
and next
, but at CAS
the time pred
by the other thread deleted, at this time the situation is the same insert
, CAS
failed, retry
- Whenever you change the structure of a linked list, you
remove
insert
need to retry the operation.
find
, you may encounter a marked delete, the item
item
remove
implementation is likely to be deleted, so you need to start the traversal
ABA Issues
The problem of ABA is still there, insert
in:
if (CAS (&pred->next, item, New_item)) { return TRUE; }
If the CAS
previous, pred
after item
is removed, and the same address value is added, but its value is changed, this time CAS
will succeed, but the list may not be ordered.pred->val < new_item->val > item->val
To solve this problem, you can use the pointer value address to align the other bits to store a count that represents pred->next
the number of changes. When insert
it pred
is taken, the pred->next
stored count is assumed to be 0, CAS
before other threads pred->next
have been removed and added back item
, and the pred->next
count in this case increases, resulting insert
in a CAS
failure.
The lowest bit remains as a delete flag #define MASK ((sizeof (node_t)-1) & ~0x01) #define GET_TAG (P) ((markable_t) P & MASK) #d Efine tag (P, Tag) ((markable_t) p | (tag)) #define MARK (P) ((markable_t) p | 0x01) #define Has_mark (P) ((markable_t) P & 0x01) #define Strip_mark (P) (( node_t*) ((markable_t) P & ~ (MASK | 0x01))
remove
Implementation of:
/* Mark before Delete * /if (! CAS (&sitem->next, Inext, MARK (Inext))) { continue; } int tag = Get_tag (pred->next) + 1; if (CAS (&pred->next, item, Tag (Strip_mark (sitem->next), tag))) { haz_defer_free (sitem); return TRUE; }
insert
The count can also be updated in pred->next
the.
Summarize
A lock-free implementation is inherently dependent on CAS
the mutex. By implementing a lock free data structure from scratch, you can feel the tricky of the lock. The final code can be obtained from GitHub here. In order to be simple, the code implements a hazard pointer that is not very powerful and can refer to the previous blog post.
Original address: http://codemacro.com/2015/05/05/lock_free_list/
Written by Kevin Lynx posted athttp://codemacro.com
The realization of the chain list without lock order