The lock-free list is the basis of many interesting data structures, the simplest hash map , where the lock-free list is used as a collision list, the split-ordered list , built entirely on the list with the original splitting algorithm of the bucket, a multi-level skip list that is essentially hierarchical list of lists. In the previous article, we made sure that you can give such an internal structure to a competitive container so that it supports thread-safe iterators in the dynamic world of lock-free containers. As we found out, the main condition for the lock-free container to become iterable is the stability of the internal structure: nodes should not be physically deleted (delete). In this case, the iterator is simply a (possibly composite) pointer to a node with the possibility of moving to the next (increment operator).template <typename T> struct list_node { std::atomic<list_node*> next_; T data_; }; template <typename T> struct node { std::atomic<node*> next_; std::atomic<T*> data_; }; data_ field of the node. As a result, our list will acquire the following internal structure:
template <typename T> struct iterator { guarded_ptr<T> gp_; node* node_; T* operator ->() { return gp_.ptr; } iterator& operator++() { // , node_, // } }; guarded_ptr (which contains a pointer to data protected by a hazard pointer) is mandatory, since we cannot guarantee that another stream will not delete data for which the iterator is positioned. It is possible to guarantee only that at the time of the iterator's transition to the node_ it (the node) contained data. The protected pointer ensures that while the iterator is on node_ , its data will not be physically deleted (delete).

guarded_ptr iterator guarantees that the data will not be physically deleted while the iterator is on the node:
node_ . In this case, the iterator is positioned on the node node_ . It would be strange if in this case operator ->() iterator returned a pointer to different data. guarded_ptr guarantees us the immutability of the data returned by the iterator, but does not at all prevent them from being changed by other threads:
data_ node field. The insert() operation of adding new data is somewhat more complicated, since two cases are possible:
data_ field to the empty prev node:
data_ - would not have thrown a couple of surprises.
linear_search function is an ordinary linear search in the list (well, not quite normal, we still have a competitive list, so we are provided with squats with atomic operations and hazard pointers), returns true if the key is found and false otherwise . The out-parameter of the pos function (of type insert_pos ) is always filled. The search stops as soon as the key of the not less sought data . We are interested in inserting a search failure — in this case, the pos will contain the insertion position. The pos.cur node pos.cur always be non-empty (or it will point to the end of the list — a dedicated tail node), but pos.prev — the previous one for pos.cur — may indicate an empty node — this is the case we are particularly interested in. The prevData and curData fields of the prevData structure are the insert_pos values ​​for prev and cur respectively. Note also that we do not need to protect pointers to the prev and cur hazard pointer nodes, since the nodes from our list are never deleted. But the data can be deleted, so we protect them.pos.prevData == nullptr ), then we try to write a link to the inserted data into it. Since our list is competitive (moreover, until it is even lock-free, but we will fix it soon), we change the pointer prev.data_ atomic CAS: if it is successful, it means that no other stream prevented us and we inserted new data to list.linear_search() executed, the insert_pos structure is initialized by the insert position, and the thread is ready to call link() , but then its time has passed and it 
pos flow A, nothing has changed: prev was empty, so empty and remained. Therefore, flow A will write data with key 30 to prev and successfully complete the insertion, thereby breaking the orderliness of the list.prev->data_ is NULL . We have the right to link new data to an empty node only when the prev -> cur bundle has not changed after the search; moreover, the data of these nodes have not changed. To solve this problem, use the marked pointer technique: the lower bit of the pointer to the node data will be used as a sign of “inserting a new node”. Moreover, it is necessary to mark both nodes — prev and cur — otherwise the other stream may delete data from cur and the list’s orderliness will again be broken (let's not forget that the cur node is always non-empty). As a result, the node structure will change and the link() function of adding data will look like template <typename T> struct node { std::atomic< node * > next_; std::atomic< marked_ptr< T, 1 >> data_; }; bool link( insert_pos& pos, T& data ) { // prev cur if ( !pos.cur->data_.CAS( marked_ptr(pos.curData, 0), marked_ptr(pos.curData, 1))) { // — - return false; } if ( !pos.prev->data_.CAS( marked_ptr(pos.prevData, 0), marked_ptr(pos.prevData, 1))) { // — // pos.cur->data_.store( marked_ptr(pos.curData, 0)); return false; } // , prev -> cur if ( pos.prev->next_.load() != pos.cur ) { // - - prev cur pos.prev->data_.store( marked_ptr(pos.prevData, 0)); pos.cur->data_.store( marked_ptr( pos.curData, 0)); return false; } // , if ( pos.prevData == nullptr ) { // // , store bool ok = pos.prev->data_.CAS( marked_ptr( nullptr, 1 ), marked_ptr( &data, 0 )); // cur pos.cur->data_.store( marked_ptr( pos.curData, 0)); return ok; } else { // — Harris/Michael // // } } data_ field): now we have to zero only unmarked data, but this does not add to the complexity of the deletion procedure. Search the list completely ignores our marker.link() function), the lock-free condition is broken, as the node will remain marked forever and someday will lead to endless active waiting for a tag to be removed. On the other hand, it is a 
link() . While A is waiting for it to be re-scheduled, other threads have time to link() function of flow A, but lead to a violation of the list sortedness.link() : after we have marked the prev and cur nodes, between which we have to insert a new one (or use an empty prev ), we must check that the insertion position has not changed . This can be checked in the only way - to find the node following the new data, and make sure that it is equal to cur : bool link( insert_pos& pos, T& data ) { // prev cur // ... // , prev -> cur // ... // , if ( find_next( data ) != pos.cur ) { // oops! … pos.prev->data_.store( marked_ptr(pos.prevData, 0)); pos.cur->data_.store( marked_ptr( pos.curData, 0)); return false; } // , // ... } find_next() searches for the node closest to data .
IterableList with support for thread-safe iterators.Source: https://habr.com/ru/post/317882/
All Articles