上一节中介绍了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