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;
}
};
上面的代码摘自&#34; C ++ Concurrency in action&#34;在这里,它使用get_tail()
来锁定tail_mutex
上的尾部。
这本书说:
事实证明,
tail_mutex
上的锁定不仅是保护尾部读取所必需的,而且还必须确保您不会从头部读取数据。如果你没有那个互斥锁,那么一个线程很可能会同时调用try_pop()
和一个调用push()
的线程,并且它们的操作没有定义的顺序。即使每个成员函数都锁定互斥锁,它们也会锁定不同的互斥锁,并且它们可能会访问相同的数据;毕竟,队列中的所有数据都来自对push()
的调用。因为线程可能在没有定义的顺序的情况下访问相同的数据,所以这将是数据争用和未定义的行为。值得庆幸的是,tail_mutex
中get_tail()
的锁定可以解决所有问题。由于对get_tail()
的调用锁定了与push()
的调用相同的互斥锁,因此两次调用之间存在已定义的顺序。在调用get_tail()
之前调用push()
,在这种情况下它会看到tail的旧值,或者在调用push()
之后发生,在这种情况下它会看到尾部的新值和附加到前一尾值的新数据。
我不太了解这一点:如果我只使用head.get() == tail
,则此比较要么在tail = new_tail
push()
之前进行,要比较head.get()
值tail
或之后将head.get()
与tail
的新值进行比较,为什么会出现数据竞争?
答案 0 :(得分:1)
我不同意这一点。 get_tail
中不应该包含任何互斥锁,这个函数本身就不容易出现数据争用,也不存在内存重新排序。实际上,应该完全取消get_tail
。尾部的用户应该保护使用不合适,但将mutex放入get tail实际上是一个可怕的反模式。当然,将互斥锁放在每个函数中都会使程序线程安全。它还将使其有效地实现单线程 - 如果需要单线程,则不要使用线程。
多线程的艺术不在于将互斥体放在任何地方。它不使用它们。