在C++11中,可以通过实例化std::mutex创建互斥量,可以通过调用成员函数lock()进行上锁,调用unlock()进行解锁。
例如:
int g_num = 0;
std::mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i)
{
//加锁
g_num_mutex.lock();
++g_num;
std::cout << id << " => " << g_num << '\n';
//若在这期间,发生异常,将导致锁无法释放,异常
//解锁
g_num_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int _tmain(int argc, _TCHAR* argv[])
{
std::thread td1(slow_increment, 0);
std::thread td2(slow_increment, 0);
td1.join();
td2.join();
}
不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况,如果处理不当,将导致锁无法释放,程序卡住。
因此,C++库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。对于以上的代码,我们只需要将全局变量用lock_guard锁即可大大提高程序的健壮性。
修改代码如下:
for (int i = 0; i < 3; ++i)
{
//使用RAII语法,提高程序的健壮性
std::lock_guard<std::mutex> gurad(g_num_mutex);
++g_num;
...
}
虽然某些情况下,使用全局变量没问题,但在大多数情况下,互斥量通常会与保护的数据放在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将互斥量放在一个类中,对类的功能进行封装,并进行数据保护。在这种情况下,互斥量和要保护的数据都被定义成private成员,这会让访问数据的代码变的清晰。
可靠的代码设计
进行代码设计时,一定要特别注意不要返回受保护数据的指针或者引用,否则数据安全是不可以靠的,例如:
template<typename T>
class CThreadsafeStack
{
public:
//不要设计这样的接口,是不安全行为
//因为用户可以在类外修改数据
std::stack<T>& GetData()
{
std::lock_guard<std::mutex> guard(m_mutex);
return m_data;
}
private:
std::mutex m_mutex; //互斥量
std::stack<T> m_data;//受保护的数据
};
接口内在的条件竞争
在C++ STL模板库中,已经实现stack功能,但是std::stack<>却不是线程安全的,因为stack成员函数存在恶性条件的数据竞争,导致多线程环境下,数据出现错误。
例如:
std::stack<int> st;
st.push(100);
st.push(200);
st.push(300);
std::thread td1([&](){
int value = st.top();
//其他操作
std::this_thread::sleep_for(std::chrono::milliseconds(5));
st.pop();
//自己实现的打印函数,保证打印互斥,
//可以用普通的printf函数实现,只是打印可能不完整
safe_print(value);
});
std::thread td2([&](){
int value = st.top();
//其他操作
std::this_thread::sleep_for(std::chrono::milliseconds(5));
st.pop();
//自己实现的打印函数,保证打印互斥。
safe_print(value);
});
td1.join();
td2.join();
运行结果:
get top value is :300
get top value is :300
由于接口之间没有锁机制或者锁的范围太小,导致两个线程对同一个数据进行两次读取(都pop之前进行top操作),导致进行数据处理的时候就出现异常情况(一个数据处理两次)。
从以上实验以及线程时间片分析,top()和pop()之间存在恶性条件竞争,因为锁的粒度太小(接口内有锁或者干脆没有锁机制),需要保护的操作并未全覆盖到导致。
如果需要达到预期效果,需要有这么一个锁,它能锁住top和pop两个操作才可以,这样锁的粒度就变大了,从设计上来说,并不合理。因为如果一个系统中锁的粒度太大,一个线程需要等待较长时间,导致系统的并发性能就受到了限制。因此原有的stack提供的接口在多线程环境中,显得并不是那么好。
为了达到线程安全以及符合栈的基本操作,需要重新设计线程安全的栈类。
代码如下:
struct empty_stack : std::exception
{
const char* what() const throw()
{
return "empty stack!";
};
};
template<typename T>
class CThreadsafeStack
{
public:
CThreadsafeStack(){}
CThreadsafeStack(const CThreadsafeStack& other)
{
std::lock_guard<std::mutex>(other.m_mutex);
m_data = other.m_data;
}
//不支持赋值运算
CThreadsafeStack& operator=(const CThreadsafeStack&) = delete;
//修改接口以规避接口之间的数据竞争(在pop里面完成top和pop)
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m_mutex);
// 在调用pop前,检查栈是否为空
if (m_data.empty()) throw empty_stack();
// 在修改堆栈前,分配出返回值
std::shared_ptr<T> const res(std::make_shared<T>(m_data.top()));
m_data.pop();
return res;
}
void pop(T& value)
{
std::lock_guard<std::mutex> guard(m_mutex);
if (m_data.empty()) throw empty_stack();
value = m_data.top();
m_data.pop();
}
void push(T new_value)
{
std::lock_guard<std::mutex> guard(m_mutex);
m_data.push(new_value);
}
bool empty() const
{
std::lock_guard<std::mutex> guard(m_mutex);
return m_data.empty();
}
std::size_t size() const//const类型
{
std::lock_guard<std::mutex> guard(m_mutex);
return m_data.size();
}
private:
mutable std::mutex m_mutex; //lock_guard传递的是引用类型
std::stack<T> m_data;
};
测试代码:
CThreadsafeStack <int> st;
//仅一个数据
st.push(1);
std::function <void (int)> func = [&](int timeout)
{
try
{
if (!st.empty())
{
std::this_thread::sleep_for(std::chrono::milliseconds(timeout));
int nValue = 0;
st.pop(nValue);
safe_print(nValue);
}
}
catch (empty_stack &e)
{
std::cout << e.what() << std::endl;
}
};
std::thread td1(func,2);
std::thread td2(func,3);
td1.join();
td2.join();
运行结果:
//弹出一个数据,另外一个报异常 符合预期
get value is :1
empty stack!.