简体   繁体   English

std :: shared_ptr的use_count()周围的全部内存屏障是否会使它成为可靠的计数器?

[英]Will full memory barriers around std::shared_ptr's use_count() make it a reliable counter?

I'm implementing a thread-safe "lazy-synchronized" Set as a linked list of nodes connected by shared_ptr's. 我正在实现一个线程安全的“惰性同步”集,它是由shared_ptr连接的节点的链表。 The algorithm is from "The Art of Multiprocessor Programming". 该算法来自“多处理器编程的艺术”。 I'm adding an is_empty() function that needs to be linearizable with the existing functions: contains(), add(), remove() . 我要添加一个is_empty()函数,该函数需要与现有函数线性化: contains(), add(), remove() In the code below, you can see remove is a 2 step process. 在下面的代码中,您可以看到remove是一个两步过程。 First it "lazy" marks the node by setting marked = nullptr , then it physically moves the linked list next pointers. 首先,它通过设置marked = nullptr来“懒惰”标记节点,然后物理地移动链接列表的next指针。

Modified classes to support is_empty() 修改后的类以支持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;
};

Relevant modified methods to support is_empty 支持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 was originally a plain bool, and LazySet.counter didn't exist. Node.marked最初是一个简单的布尔值,而LazySet.counter不存在。 The choice to make them shared_ptrs was to be able to be able to atomically modify both a counter on the number of nodes and the lazy removal mark on the nodes. 选择它们为shared_ptrs是为了能够原子地修改节点数上的计数器和节点上的延迟删除标记。 Modifying both simultaneously in remove() is necessary for is_empty() to be linearizable with contains() . 为了使is_empty()可以通过contains()线性化,必须在remove()同时修改两者。 (It can't be a separate bool mark and int counter without a double wide CAS or something.) I hope to implement the counter with shared_ptr's use_count() function, but in multithreaded contexts it's only an approximation due to relaxed_memory_order . (没有双倍宽CAS或其他内容,它不能是单独的bool标记和int计数器。)我希望使用shared_ptr的use_count()函数实现该计数器,但是在多线程上下文中,由于relaxed_memory_order而仅仅是一个近似值。

I know standalone fences are usually bad practice, and I'm not too familiar with using them. 我知道独立的栅栏通常是不好的做法,并且我对使用它们不太熟悉。 But if I implemented is_empty like below, would the fences ensure it's no longer an approximation, but an exact value for a reliable counter? 但是,如果我像下面那样实现is_empty ,围栏是否会确保它不再是近似值,而是一个可靠计数器的确切值?

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
}

I only ask because LWG Issue 2776 says: 我之所以只问是因为LWG第2776期说:

We can't make use_count() reliable without adding substantially more fencing. 如果不添加更多的防护,我们就无法使use_count()可靠。

Relaxed memory order isn't the problem here. 轻松的内存顺序不是这里的问题。 use_count is not "reliable" because, by the time the value has been returned, it may have changed . use_count不是“可靠的”,因为在返回值之前,该值可能已更改 There is no data race on getting the value itself, but there's nothing preventing that value from being modified before any conditional statement based on that value. 获取值本身并没有引起数据争夺,但是也没有阻止该值在基于该值的条件语句之前被修改的任何措施。

So you can't do anything with it which relies on its value still being meaningful (with the exception that, if you're still holding a shared_ptr instance, then the use count won't go to 0). 因此,您不能依靠它的值仍然有意义来对其进行任何操作(例外是,如果您仍持有shared_ptr实例,则使用计数不会变为0)。 The only way to make it reliable is to prevent it from being changed. 使其可靠的唯一方法是防止对其进行更改。 So you'd need to have a mutex. 因此,您需要一个互斥锁。

And that mutex would have to lock, not just around the use_count call and usage, but also every time you hand out one of these shared_ptr s that you're getting the use_count from. 而且该互斥锁不仅要锁定use_count调用和用法,而且还必须在每次您从其中获取use_count的这些shared_ptr之一use_count

// ## SOME FULL MEMORY BARRIER
if (counter.use_count() == 1) {
    // ## SOME FULL MEMORY BARRIER

With an acquire fence before, you can make sure you can make sure you "see" the results of all the resets (including during assignment and destruction) of all owners in other threads. 之前使用获取隔离墙,可以确保可以“看到”其他线程中所有所有者的所有重置结果(包括在分配和销毁过程中)。 The acquire fence gives all following relaxed operation acquire semantics, preventing them from "fetching values in the future" (which is semantic insanity in any case and probably makes all programs formally UB). 获取围栏为所有随后的轻松操作提供了获取语义,从而防止它们“将来获取值”(无论如何,这是语义上的混乱,可能使所有程序正式为UB)。

(There is no meaningful fence that you could put after the call.) (通话后,您不能放置任何有意义的栅栏。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM