多线程编程-- 线程安全的栈 stack

来源:互联网 发布:eve作战网络装备 编辑:程序博客网 时间:2024/06/02 10:12

首先看看 std::stack 容器的实现

template<typename T,typename Container=std::deque<T> >class stack{public:  explicit stack(const Container&);  explicit stack(Container&& = Container());  template <class Alloc> explicit stack(const Alloc&);  template <class Alloc> stack(const Container&, const Alloc&);  template <class Alloc> stack(Container&&, const Alloc&);  template <class Alloc> stack(stack&&, const Alloc&);  bool empty() const;  size_t size() const;  T& top();  T const& top() const;  void push(T const&);  void push(T&&);  void pop();  void swap(stack&&);};

stack 是以deque 做为底部结构, 将其接口改变,使得其符合“”先进后出“”的特点。
deque 是双向开口的数据结构, 将deque 封闭其头端开口,便形成了stack, 因此便以
deque 作为缺省情况下的stack 底部结构。

在单线程下,上面的stack 是安全的,但是在多线程下的情况是如何的呢?

看下面一段代码:

stack<int> s;if (! s.empty()){    // 1  int const value = s.top();    // 2  s.pop();    // 3  do_something(value);}

以上是单线程安全代码:对一个空栈使用top()是未定义行为。对于共享的栈对象,这样的调用顺序就不再安全了,因为在调用empty()①和调用top()②之间,可能有来自另一个线程的pop()调用并删除了最后一个元素。这是一个经典的条件竞争,使用互斥量对栈内部数据进行保护,但依旧不能阻止条件竞争的发生,这就是接口固有的问题。
怎么解决呢?问题发生在接口设计上,所以解决的方法也就是改变接口设计。有人会问:怎么改?在这个简单的例子中,当调用top()时,发现栈已经是空的了,那么就抛出异常。虽然这能直接解决这个问题,但这是一个笨拙的解决方案,这样的话,即使empty()返回false的情况下,你也需要异常捕获机制。本质上,这样的改变会让empty()成为一个多余函数。

仔细观察过代码, 就会发现另一个潜在的条件竞争在调用top()②和pop()③之间
假设两个线程运行前面的代码。并且都引用同一个对象stacks;
假设,一开始栈中只有两个元素,这时任一线程上的empty()和top()都存在竞争,只需要考虑可能的执行顺序即可。
当栈被一个内部互斥量所保护时,只有一个线程可以调用栈的成员函数,所以调用可以很好地交错,并且do_something()是可以并发运行的。在表3.1中,展示一种可能的执行顺序。

这里写图片描述

当线程运行时,调用两次top(),栈没被修改,所以每个线程能得到同样的值。不仅是这样,在调用top()函数调用的过程中(两次),pop()函数都没有被调用。这样,在其中一个值再读取的时候,虽然不会出现“写后读”的情况,但其值已被处理了两次。这种条件竞争,比未定义的empty()/top()竞争更加严重;虽然其结果依赖于do_something()的结果,但因为看起来没有任何错误,就会让这个Bug很难定位.

解决方案 基本思想就是 将 top() 和pop() 这两个操作合成一步操作。

方案1: 传入一个引用
第一个选项是将变量的引用作为参数,传入pop()函数中获取想要的“弹出值”:

std::vector result;
some_stack.pop(result);
大多数情况下,这种方式还不错,但有明显的缺点:需要临时构造出一个堆中类型的实例,用于接收目标值。对于一些类型,这样做是不现实的,因为临时构造一个实例,从时间和资源的角度上来看,都是不划算。对于其他的类型,这样也不总能行得通,因为构造函数需要的一些参数,在代码的这个阶段不一定可用。最后,需要可赋值的存储类型,这是一个重大限制:即使支持移动构造,甚至是拷贝构造(从而允许返回一个值),很多用户自定义类型可能都不支持赋值操作。

方案2:返回指向弹出值的指针
第二个选择是返回一个指向弹出元素的指针,而不是直接返回值。指针的优势是自由拷贝,并且不会产生异常, 缺点就是返回一个指针需要对对象的内存分配进行管理,对于简单数据类型(比如:int),内存管理的开销要远大于直接返回值。对于选择这个方案的接口,使用std::shared_ptr是个不错的选择;不仅能避免内存泄露(因为当对象中指针销毁时,对象也会被销毁),而且标准库能够完全控制内存分配方案,也就不需要new和delete操作。这种优化是很重要的:因为堆栈中的每个对象,都需要用new进行独立的内存分配,相较于非线程安全版本,这个方案的开销相当大

下面给出一个线程安全的stack
它实现了方案1和方案2:重载了pop(),使用一个局部引用去存储弹出值,并返回一个std::shared_ptr<>对象。它有一个简单的接口,只有两个函数:push()和pop();+

#include <memory>#include <mutex>#include <stack>struct empty_stack: std::exception{  const char* what() const throw() {    return "empty stack!";  };};template<typename T>class threadsafe_stack{private:   std::stack<T>data_;   mutable MutexLock lock_;public:    threadsafe_stack():data_(std::stack<int>()) {}    threadsafe_stack()(const threadsafe_stack& other) {      LockGuard<MutexLock>(&other.lock_);      data_ = other.data_ ; //    }threadsafe_stack & operator=(const threadsafe_stack&)= delete;void push(T new_value) {   LockGuard<MutexLock> lock(&lock_);   data_.push(new_value);}std::shared_ptr<T> pop() {   LockGuard<MutexLock> lock (&lock_);   if (data_.empty()) throw empty_stack(); // 在调用pop 前, 检查栈是否为空    std:: shared_ptr <T> const res(std::make_shared<T>(data_.top()));//在修改栈前,分配出返回值   data_.pop();   return res;} void pop(T& value)  {    LockGuard<MutexLock> lock(&lock_);    if(data_.empty()) throw empty_stack();    value=data_.top();    data_.pop();  }  bool empty() const  {    LockGuard<MutexLock> lock(&lock_);    return data_.empty();  }};

参考C++ 并发编程
https://www.gitbook.com/book/chenxiaowei/cpp_concurrency_in_action/details

0 0