上一节中介绍了mutex的基本使用方法,使用mutex来保护共享数据并不能解决race condition带来的问题,假如我们有一个堆栈数据结构类似于std::stack它提供了5个基本操作push(),pop(),top(),empty(),和size()。这里的top()操作返回栈顶元素的拷贝,这样我们就可以使用一个mutex来保护栈内部的数据。但是race codition情况下,虽然使用mutex在stack的每个接口内都对共享数据进行了保护,仍然有问题存在。
#include <deque> 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&&); }; int main() {}
这里的问题在于,empty()和size()的返回值是不可靠的,虽然在我们调用这两个函数时,它们的返回值是正确的,但是一旦返回,其它的线程就可以访问stack并push()新的数据到堆栈中,基于empty()和size()之前的返回值就可能导致问题。比如如下的情况,线程A和线程B获取了同一个栈顶数据的拷贝,线程A执行pop()将其弹出,线程B执行,就将栈顶之下第二个数据直接弹出了。这样导致这个数据并没有得到处理。
因此,我们需要对接口进行重构。这里采用的方案是提供两种pop()接口,一种接受数据的引用,在pop()内将栈顶数据赋值给这个引用参数。另一个实现则是返回指向栈顶数据的指针。第一种接口大多数情况下是可行的,但是缺点是使用者需要先构造一个数据对象的实例,还要求这个对象是可以赋值的。第二种接口则不需要以传值的方式返回数据,但使用者要注意对指针的使用以避免内存泄露等问题。
重构后的stack头文件如下:
#include <exception> #include <memory> struct empty_stack: std::exception { const char* what() const throw(); }; template<typename T> class threadsafe_stack { public: threadsafe_stack(); threadsafe_stack(const threadsafe_stack&); threadsafe_stack& operator=(const threadsafe_stack&) = delete; void push(T new_value); std::shared_ptr<T> pop(); void pop(T& value); bool empty() const; }; int main() {}由于stack不支持赋值操作,因此将其定义为delete。
stack的实现如下:
#include <exception> #include <stack> #include <mutex> #include <memory> 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 std::mutex m; public: threadsafe_stack(){} threadsafe_stack(const threadsafe_stack& other) { std::lock_guard<std::mutex> lock(other.m); data=other.data; } threadsafe_stack& operator=(const threadsafe_stack&) = delete; void push(T new_value) { std::lock_guard<std::mutex> lock(m); data.push(new_value); } std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); std::shared_ptr<T> const res(std::make_shared<T>(data.top())); data.pop(); return res; } void pop(T& value) { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); value=data.top(); data.pop(); } bool empty() const { std::lock_guard<std::mutex> lock(m); return data.empty(); } }; int main() { threadsafe_stack<int> si; si.push(5); si.pop(); if(!si.empty()) { int x; si.pop(x); } }堆栈可以被拷贝,在拷贝构造函数中,使用mutex来对内部数据进行保护。为了保证内部数据被mutex保护,不能使用初始化参数列表来初始化堆栈的成员变量。
使用mutex要注意粒度问题,保护的粒度太小,会漏掉一些场景导致race condition。保护粒度太大则会降低并发线程的执行效率。要达到粒度适当,则可能需要多个mutex,使用多个mutex又有可能导致死锁问题。下一节,我们再看看死锁是怎么回事,以及怎么解决死锁问题。
版权声明:本文为博主原创文章,未经博主允许不得转载。
[C++11 并发编程] 06 Mutex race condition
原文地址:http://blog.csdn.net/yamingwu/article/details/47423257