我正在实现一个线程安全的“惰性同步”集,它是由shared_ptr连接的节点的链表。该算法来自“多处理器编程的艺术”。我要添加一个is_empty()
函数,该函数需要与现有函数contains(), add(), remove()
线性化。在下面的代码中,您可以看到remove
是一个两步过程。首先,它通过设置marked = nullptr
“惰性”标记节点,然后物理地移动链接列表next
指针。
修改后的类以支持is_empty()
template <class T>
class LazySet : public Set<T> {
public:
LazySet ();
bool contains (const T&) const;
bool is_empty () const;
bool add (const T&);
bool remove (const T&);
private:
bool validate(const std::shared_ptr<Node>&, const std::shared_ptr<Node>&);
class Node;
std::shared_ptr<Node> head;
std::shared_ptr<bool> counter; //note: type is unimportant, will never change true/fase
};
template <class T>
class LazySet<T>::Node {
public:
Node ();
Node (const T&);
T key;
std::shared_ptr<bool> marked; //assume initialized to = LazySet.counter
// nullptr means it's marked; otherwise unmarked
std::shared_ptr<Node> next;
std::mutex mtx;
};
支持is_empty的相关修改方法
template <class T>
bool LazySet<T>::remove(const T& k) {
std::shared_ptr<Node> pred;
std::shared_ptr<Node> curr;
while (true) {
pred = head;
curr = atomic_load(&(head->next));
//Find window where key should be in sorted list
while ((curr) && (curr->key < k)) {
pred = atomic_load(&curr);
curr = atomic_load(&(curr->next));
}
//Aquire locks on the window, left to right locking prevents deadlock
(pred->mtx).lock();
if (curr) { //only lock if not nullptr
(curr->mtx).lock();
}
//Ensure window didn't change before locking, and then remove
if (validate(pred, curr)) {
if (!curr) { //key doesn't exist, do nothing
//## unimportant ##
} else { //key exists, remove it
atomic_store(&(curr->marked), nullptr); //logical "lazy" remove
atomic_store(&(pred->next), curr->next) //physically remove
(curr->mtx).unlock();
(pred->mtx).unlock();
return true;
}
} else {
//## unlock and loop again ##
}
}
}
template <class T>
bool LazySet<T>::contains(const T& k) const {
std::shared_ptr<Node> curr;
curr = atomic_load(&(head->next));
//Find window where key should be in sorted list
while ((curr) && (curr->key < k)) {
curr = atomic_load(&(curr->next));
}
//Check if key exists in window
if (curr) {
if (curr->key == k) { //key exists, unless marked
return (atomic_load(&(curr->marked)) != nullptr);
} else { //doesn't exist
return false;
}
} else { //doesn't exist
return false;
}
}
Node.marked
最初是一个简单的布尔语言,而LazySet.counter
不存在。选择它们为shared_ptrs是为了能够原子地修改节点数上的计数器和节点上的延迟删除标记。要使remove()
用is_empty()
线性化,必须同时在contains()
中进行修改。 (没有双倍宽CAS或其他内容,它不能是单独的布尔标记和整数计数器。)我希望使用shared_ptr的use_count()
函数来实现该计数器,但是在多线程上下文中,由于{{1 }}。
我知道独立的围栏通常是不好的做法,并且我对使用它们不太熟悉。 但是,如果我像下面那样实现relaxed_memory_order
,围栏会确保它不再是一个近似值,而是一个可靠计数器的确切值?
is_empty
我之所以问是因为LWG Issue 2776说:
如果不添加更多的防护措施,我们将无法使
template <class T> bool LazySet<T>::is_empty() const { // ## SOME FULL MEMORY BARRIER if (counter.use_count() == 1) { // ## SOME FULL MEMORY BARRIER return true } // ## SOME FULL MEMORY BARRIER return false }
可靠。
答案 0 :(得分:0)
轻松的存储顺序不是这里的问题。 use_count
是“不可靠的”,因为在返回值之前,它可能已更改。获取值本身并没有引起数据争夺,但是也没有阻止该值在基于该值的任何条件语句之前被修改的事情。
因此,您不能依靠它的值仍然有意义来对其进行任何操作(例外是,如果您仍然持有一个shared_ptr
实例,则使用计数不会变为0 )。使其可靠的唯一方法是防止对其进行更改。因此,您需要一个互斥锁。
该互斥锁不仅要锁定use_count
的调用和用法,而且还必须每次您分发这些shared_ptr
中的一个时就锁定,{{1} }。
答案 1 :(得分:-2)
// ## SOME FULL MEMORY BARRIER
if (counter.use_count() == 1) {
// ## SOME FULL MEMORY BARRIER
使用以前的获取隔离墙,可以确保可以“看到”其他线程中所有所有者的所有重置结果(包括分配和销毁过程)。获取栅栏为随后进行的所有宽松操作提供了获取语义,从而防止它们“在将来获取值”(无论如何,这都是语义上的混乱,可能会使所有程序正式为UB)。
(通话后不能放置任何有意义的围栏。)