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