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()的调用。因为线程可能访问相同的数据而没有定义的顺序,这将是一个数据竞争和未定义的行为。谢天谢地,tail_mutex在get_tail()中的锁解决了所有问题。因为对get_tail()的调用与对push()的调用锁定相同的互斥对象,所以这两个调用之间有一个定义的顺序。对get_tail()的调用发生在对push()的调用之前,在这种情况下,它看到尾的旧值,或者在调用push()之后发生,在这种情况下,它会看到尾的新值和附加到以前的尾值的新数据。
我不太明白这一点:如果我只使用head.get() == tail,这种比较要么发生在tail = new_tail in push()中,将head.get()与旧的tail值进行比较,要么在将head.get()与tail的新值进行比较之后,为什么会出现数据竞争?
发布于 2015-10-26 13:39:09
我不同意这点。get_tail不应该有任何互斥,这个函数本身不容易发生数据竞争,也不容易发生内存重排。事实上,应该完全消除get_tail。用户的尾巴应该保护使用作为学徒,但把互斥进入获取尾巴实际上是一个可怕的反模式。当然,在每个函数中添加互斥对象将使您的程序线程安全。它还将使它有效地单线程-如果单线程是需要的,只需不使用线程。
多线程的艺术并不在于把互斥放到任何地方。它是在不使用它们。
https://stackoverflow.com/questions/33341492
复制相似问题