下面以 std::mutex 为例介绍 C++11 中的互斥量用法。
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
mutex.cpp
#include // std::cout
#include // std::thread
#include // std::mutex
volatile int counter(0); // non-atomic counter
std::mutex mutex; // locks access to counter
//定义线程执行函数
void thread_func()
{
mutex.lock(); //对mutex上锁(如果mutex已经被占用,则阻塞在此处)
++counter; //执行操作
mutex.unlock();//对mutex解锁
}
int main (int argc, const char* argv[])
{
mutex.lock(); //对mutex上锁
std::thread subthread(thread_func); //创建一个子线程,线程执行函数为thread_func
std::cout<<"before counter = "<<counter<<std::endl; //解锁前
mutex.unlock(); //对mutex解锁(启动子线程执行)
if (subthread.joinable()) { //回收子线程
subthread.join();
std::cout<<"子线程已回收"<<std::endl;
}
std::cout<<"after counter = "<<counter<<std::endl; //解锁前
return 0;
}
编译 && 执行:
g++ mutex.cpp -lpthread’
./a.out
执行结果:

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。
try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
time_mutex.cpp
#include // std::cout
#include // std::thread
#include // std::mutex
volatile int counter(0); // non-atomic counter
std::timed_mutex mutex; // locks access to counter
//定义线程执行函数
void thread_func()
{
while (!mutex.try_lock_for(std::chrono::milliseconds(499))) {
std::cout << "子线程等待中..."<<std::endl;
}
++counter; //执行操作
mutex.unlock();//对mutex解锁
}
int main (int argc, const char* argv[])
{
mutex.lock(); //对mutex上锁
std::thread subthread(thread_func); //创建一个子线程,线程执行函数为thread_func
std::cout<<"before counter = "<<counter<<std::endl; //解锁前
std::this_thread::sleep_for(std::chrono::milliseconds(2000)); //主线程阻塞执行2s
mutex.unlock(); //对mutex解锁(启动子线程执行)
if (subthread.joinable()) { //回收子线程
subthread.join();
std::cout<<"子线程已回收"<<std::endl;
}
std::cout<<"after counter = "<<counter<<std::endl; //解锁前
return 0;
}
编译 && 执行:
g++ time_mutex.cpp -lpthread’
./a.out
执行结果:

std::lock_guard是C++11提供的锁管理器,可以管理std::mutex,也可以管理其他常见类型的锁。
std::lock_guard的对锁的管理属于RAII风格用法(Resource Acquisition Is Initialization),在构造函数中自动绑定它的互斥体并加锁,在析构函数中解锁,大大减少了死锁的风险。
lock_guard.cpp
#include
#include
#include
int loop_cnt = 10;
class Widget
{
public:
Widget() = default;
~Widget() = default;
void addCnt()
{
std::lock_guard<std::mutex> cLockGurad(lock_); //构造时加锁,析构时解锁
std::cout<<"线程执行addCnt函数"<<std::endl;
// lock_.lock(); //不使用lock_guard时的写法
for (int i = 0; i < loop_cnt; i++)
{
cnt++;
std::cout << "cnt =" << cnt << std::endl;
}
// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
}
int cnt = 0; //类成员函数,属于对象
private:
std::mutex lock_; //mutex定义为类成员变量
};
void ThreadMain1(Widget *pw)
{
std::cout << "thread 1 begin." << std::endl;
pw->addCnt();
std::cout << "thread 1 end." << std::endl;
}
void ThreadMain2(Widget *pw)
{
std::cout << "thread 2 begin." << std::endl;
pw->addCnt();
std::cout << "thread 2 end." << std::endl;
}
int main()
{
Widget *pw = new Widget(); //定义一个对象指针
std::thread t1(&ThreadMain1, pw); //开启两个子线程
std::thread t2(&ThreadMain2, pw);
t1.join(); //回收;两个子线程
t2.join();
std::cout << "cnt =" << pw->cnt << std::endl;
return 0;
}
编译 && 执行:
g++ lock_guard.cpp -lpthread’
./a.out
执行结果:

当把mutex加锁语句屏蔽之后再进行测试,修改如下:
void addCnt()
{
//std::lock_guard cLockGurad(lock_); //构造时加锁,析构时解锁
std::cout<<"线程执行addCnt函数"<<std::endl;
// lock_.lock(); //不使用lock_guard时的写法
for (int i = 0; i < loop_cnt; i++)
{
cnt++;
std::cout << "cnt =" << cnt << std::endl;
}
// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
}
执行结果:

可以看出在加锁的情况下(上图),两个线程是互斥的执行对象的addCnt()函数,当线程1执行完并且退出解锁后,线程2才可以执行addCnt()函数,通过上述的加锁(mutex)机制可以保证addCnt()函数是线程安全的。
可以看出在不加锁的情况下(下图),两个线程是交叉执行的,对于cnt共有变量的处理结果是未知的,因此addCnt()函数是非线程安全的。
总结:因此对于同一个类对象在不同线程中执行时需要考虑对共享变量的原子化处理,同时考虑成员函数的线程安全执行,通常考虑在类中定义mutex变量,通过mutex互斥变量的加锁实现函数的多线程安全执行。
std::unique_lock为锁管理模板类,是对通用mutex的封装。std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。
std::unique_lock还支持同时锁定多个mutex,这避免了多道加锁时的资源”死锁”问题。在使用std::condition_variable时需要使用std::unique_lock而不应该使用std::lock_guard。
std::unique_lock类成员函数介绍:
(1). unique_lock构造函数:禁止拷贝构造,允许移动构造;
(2). operator =:赋值操作符,允许移动赋值,禁止拷贝赋值;
(3). operator bool:返回当前std::unique_lock对象是否获得了锁;
(4). lock函数:调用所管理的mutex对象的lock函数;
(5). try_lock函数:调用所管理的mutex对象的try_lock函数;
(6).try_lock_for函数:调用所管理的mutex对象的try_lock_for函数;
(7).try_lock_until函数:调用所管理的mutex对象的try_lock_until函数;
(8). unlock函数:调用所管理的mutex对象的unlock函数;
(9). release函数:返回所管理的mutex对象的指针,并释放所有权,但不改变mutex对象的状态;
(10). owns_lock函数:返回当前std::unique_lock对象是否获得了锁;
(11). mutex函数:返回当前std::unique_lock对象所管理的mutex对象的指针;
(12). swap函数:交换两个unique_lock对象。
#include
#include
#include
int loop_cnt = 10;
class Widget
{
public:
Widget() = default;
~Widget() = default;
void addCnt()
{
std::unique_lock<std::mutex> uniquelock(lock_); //构造时加锁,析构时解锁
std::cout<<"线程执行addCnt函数"<<std::endl;
// lock_.lock(); //不使用lock_guard时的写法
for (int i = 0; i < loop_cnt; i++)
{
cnt++;
std::cout << "cnt =" << cnt << std::endl;
}
// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
}
int cnt = 0; //类成员函数,属于对象
private:
std::mutex lock_; //mutex定义为类成员变量
};
void ThreadMain1(Widget *pw)
{
std::cout << "thread 1 begin." << std::endl;
pw->addCnt();
std::cout << "thread 1 end." << std::endl;
}
void ThreadMain2(Widget *pw)
{
std::cout << "thread 2 begin." << std::endl;
pw->addCnt();
std::cout << "thread 2 end." << std::endl;
}
int main()
{
Widget *pw = new Widget(); //定义一个对象指针
std::thread t1(&ThreadMain1, pw); //开启两个子线程
std::thread t2(&ThreadMain2, pw);
t1.join(); //回收;两个子线程
t2.join();
std::cout << "cnt =" << pw->cnt << std::endl;
return 0;
}
unique_lock和lock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以在需要的时候进行lock或者unlock调用,不非得是析构或者构造时。它们的区别可以通过成员函数就可以一目了然。

另外,在条件变量condition_variable中只能使用std::unique_lock