为啥 get_tail() 应该使用 tail_mutex 上的锁?

Posted

技术标签:

【中文标题】为啥 get_tail() 应该使用 tail_mutex 上的锁?【英文标题】:why should get_tail() use the lock on tail_mutex?为什么 get_tail() 应该使用 tail_mutex 上的锁? 【发布时间】:2015-10-26 08:37:00 【问题描述】:
template<typename T>
class threadsafe_queue

private:
    struct node
    
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
    ;

    std::mutex head_mutex;
    std::unique_ptr<node> head;
    std::mutex tail_mutex;
    node* tail;

    node* get_tail()
    
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
        return tail;
    

    std::unique_ptr<node> pop_head()
    
        std::lock_guard<std::mutex> head_lock(head_mutex);
        // is it necessary to use get_tail()
        if(head.get()==get_tail()) 
        
            return nullptr;
        
        std::unique_ptr<node> const old_head=std::move(head);
        head=std::move(old_head->next);
        return old_head;
    


public:
    threadsafe_queue():
        head(new node),tail(head.get())
    

    threadsafe_queue(const threadsafe_queue& other)=delete;
    threadsafe_queue& operator=(const threadsafe_queue& other)=delete;

    std::shared_ptr<T> try_pop()
    
        std::unique_ptr<node> old_head=pop_head();
        return old_head?old_head->data:std::shared_ptr<T>();
    

    void push(T new_value)
    
        std::shared_ptr<T> new_data(
            std::make_shared<T>(std::move(new_value)));
        std::unique_ptr<node> p(new node);
        node* const new_tail=p.get();
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
        tail->data=new_data;
        tail->next=std::move(p);
        tail=new_tail;
    
;

以上代码摘自第 162 页的“C++ Concurrency in action”。这里它使用get_tail() 来获取锁定tail_mutex 的尾部。

书上说:

事实证明,tail_mutex 上的锁定不仅是保护对 tail 本身的读取所必需的,而且还需要确保不会发生从头部读取数据的数据竞争。如果你没有那个互斥锁,一个线程很可能同时调用try_pop() 和一个线程调用push(),并且它们的操作没有定义的顺序。尽管每个成员函数都持有互斥锁,但它们持有不同互斥锁的锁,并且它们可能访问相同的数据;毕竟,队列中的所有数据都来自对push() 的调用。因为线程可能会在没有定义顺序的情况下访问相同的数据,所以这将是数据竞争和未定义的行为。谢天谢地,get_tail() 中的 tail_mutex 的锁定解决了所有问题。因为对get_tail() 的调用与对push() 的调用锁定了相同的互斥锁,所以两个调用之间有一个定义的顺序。对get_tail() 的调用发生在对push() 的调用之前,在这种情况下它会看到tail 的旧值,或者在对push() 的调用之后发生,在这种情况下它会看到tail 的新值和新数据附加到之前的尾部值。

我不太明白:如果我只使用head.get() == tail,这种比较要么发生在push() 中的tail = new_tail 之前,以将head.get()tail 的旧值进行比较,要么在之后比较@ 987654340@ 新值为tail,为什么会有数据竞争?

【问题讨论】:

if i just use head.get() == tail, this comparison either happens before tail = new_tail in push() to compare head.get() with the old value of tail or after to compare head.get() with the new value of tail, why would there be a data race? - 这是定义的数据竞争:访问相同的变量 (tail) 并发 (通过push()get_tail() 方法)和一个访问(通过push())是写访问。在get_tail() 方法中使用tail_mutex 消除了这种并发性和其他问题,如书中给定的引用所述。 【参考方案1】:

我不同意这一点。 get_tail 里面不应该有任何互斥锁,这个函数本身不存在数据竞争倾向,也不存在内存重新排序倾向。事实上,get_tail 应该被完全删除。 tail 的用户应该适当地保护使用,但是将互斥锁放在 get tail 实际上是一种可怕的反模式。将互斥锁放在每个函数中当然会让你的程序线程安全。它还将使其有效地成为单线程 - 如果需要单线程,请不要使用线程。

多线程的艺术并不在于将互斥锁无处不在。它是在不使用它们。

【讨论】:

感谢提问,迟到了很抱歉

以上是关于为啥 get_tail() 应该使用 tail_mutex 上的锁?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 DefaultMessageListenerContainer 不应该使用 CachingConnectionFactory?

为啥/何时应该使用静态声明变量?

我应该使用 createRef 还是 useRef,为啥?

我应该使用 FxCop,为啥?

为啥不应该使用 `'` 来转义单引号?

为啥我应该使用单独的测试目标来运行 XCTests,我应该怎么做?