1、C++11用实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况。
C++标准库为互斥量提供了一个RAII语法的模板类 std::lack_guard ,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // 3
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // 4
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end()
;
}
大多数情况下,互斥量通常会与保护的数据放在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将其放在一个类中,就可让他们联系在一起,也可对类的功能进行封装,并进行数据保护。在这种情况下,函数
add_to_list和list_contains可以作为这个类的成员函数。互斥量和要保护的数据,在类中都需要定义为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成员函数存在恶性条件的数据竞争,导致多线程环境下,数据出现错误。
比如两个线程同时完成取栈顶元素打印并且弹出栈顶元素。有可能打印出两个一样的数字。这是由于接口之间没有锁机制或者锁的范围太小,导致两个线程对同一个数据进行两次读取(都pop之前进行top操作),top()和pop()之间存在恶性条件竞争。
如果需要达到预期效果,需要有这么一个锁,它能锁住top和pop两个操作才可以,这样锁的粒度就变大了,从设计上来说,并不合理。因为如果一个系统中锁的粒度太大,一个线程需要等待较长时间,导致系统的并发性能就受到了限制。
线程已持有std::mutex锁时,若该线程再次获取该锁将出现异常,此时应使用
递归锁std::recursive_mutex。
死锁
一般在两个或者多个线程拥有对方的锁资源时却相互等待另一方释放所拥有的锁资源。进而产生死锁。这个情况常出现在需要锁定两个或者更多互斥体或其他原语时以执行操作时最常出现的情形。其他的情形也包括等待相互的另一方线程退出或者其他资源或同步需求时。
死锁避免:
按照同样的顺序对mutex加锁;
std::lock()函数。这个函数使用死锁避免算法,来任意地锁定一些可加锁的对象,就像mutex等。
std::lock(_mu, _mu2);
std::lock_guard<mutex> locker(_mu, std::adopt_lock);
std::lock_guard<mutex> locker2(_mu2, std::adopt_lock);
避免在申请到锁之后,释放锁之前调用用户函数。