为什么get_tail()要使用tail_mutex上的锁?

why should get_tail() use the lock on tail_mutex?

本文关键字:tail mutex get 为什么      更新时间:2023-10-16
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++并发操作"。这里它使用get_tail()获得锁定在tail_mutex上的尾部。

书上说:

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

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

我不同意。get_tail不应该有任何互斥锁,这个函数本身不容易发生数据竞争,也不容易发生内存重排序。事实上,get_tail应该完全取消。tail的用户应该适当地保护它的使用,但是将互斥锁放在gettail中实际上是一种糟糕的反模式。在每个函数中加入互斥锁当然会使你的程序线程安全。它还将使它成为有效的单线程——如果需要单线程,就不要使用线程。

多线程的艺术不在于到处放互斥锁。