多线程编程-- 线程安全的栈 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
- 多线程编程-- 线程安全的栈 stack
- 多线程编程 -- 线程安全的链表
- 多线程编程-- 线程安全的queue II
- Java的多线程编程模型2--怎样才线程安全
- linux多线程编程(C):信号量实现的线程安全队列
- linux多线程编程(C):互斥量实现的线程安全队列
- Java的多线程编程模型2--怎样才线程安全
- 多线程编程艺术(2)-安全的终止线程
- 网络多线程-线程的安全
- Java多线程编程与线程安全
- c++11多线程编程---线程安全队列
- 多线程编程之线程安全退出
- 设计安全的多线程应用程序(线程安全)
- Java多线程编程(五)-并发编程原理(写线程安全的Java代码)
- 《多线程编程》学习之一:使用多线程及线程安全
- C++并发实战17:线程安全的stack和queue
- 《多线程编程》学习之十:定时器Timer的使用,线程安全的单例模式
- 《Linux多线程服务端编程》—线程安全的对象生命期管理
- 杭电2033问题
- post乱码、get乱码问题如何解决?
- Activity的启动模式
- Android Studio: Plugin with id 'android-library' not found
- 不使用SDK生成BSP手动建立zynq软件工程
- 多线程编程-- 线程安全的栈 stack
- 图论之Flody
- jQuery Demo【寻找房祖名】
- JavaScript数组去重的6个方法
- Three.js Basic examples(1)
- View及ViewGroup的事件分发及传递(二)
- 记录学习感悟
- mac book air 外接USB无线网卡TP-LINK TL WN821N
- node.js笔记(2)