一、简介
多线程同时访问公共变量时,必须对变量进行保护,避免同时的“写-写”和“读-写”访问,
但变量本身是无法被保护的,毕竟变量只是一段可读写的内存区域而已。
于是,我们定义了一种新的变量 —— 互斥量//mutex,这是一种可以被保护的量。
互斥量是一种同步原语,是一种线程同步的手段,用来保护多线程同时访问的共享数据.
- std::mutex: 独占的互斥量,不能递归使用.
- std::timed_mutex: 带超时的独占互斥量,不能递归使用.
- std::recursive_mutex: 递归互斥量,不带超时功能.
- std::recursive_timed_mutex: 带超时的递归互斥量.
这些互斥量的基本接口十分相近,都是通过lock()来阻塞线程,直到获得互斥量的所有权为止。在线程或的互斥量并完成任务后,就必须使用unlock()来解除对互斥量的占用,lock和unlock必须成对出现。try_lock()尝试锁定互斥量,成功返回true,失败返回false,他是非阻塞的。
所谓保护,即:同一时间只能被一个函数(或一段代码)访问,访问的方式是“mutex.lock()”;访问过后必须要释放以便别人去访问,释放的方式是“mutex.unlock()”。
我们约定:所有的函数访问公共变量时,必须先拿起互斥量——执行mutex.lock(),才有资格进行访问;访问结束后,必须放下互斥量——执行mutex.unlock()。
理解:
- 之所以会出现所谓的“互斥量(mutex)”,完全是因为我们进行多线程编成而导致的(两个线程都会对某一个全局变量操作,比如:我想让全局变量a = a+1, A线程确实写的程序是a = a+1,但在B线程中,a = 100, 这就矛盾了);如果系统是单线程,程序只需要按照顺序执行就可以了,根据不需要互斥量!
- 针对这个问题,当我们把 a = a+1 放在 “mutex.lock()”和“mutex.unlock()”之间时,只会执行a = a+1,而不会执行 线程B中的a = 100;
- 再次提醒,互斥量(mutex)是针对全局变量,全局变量...........,记住啦
你可以想象一个将军想要从皇帝手里调兵(公共变量),那他必须拿到兵符,兵符只有一个,将军A用的时候,将军B就不能用了;将军A用完,必须还给皇帝,以便其他将军使用。兵符,就是C++中的互斥量。
这样,我们间接实现了对公共变量的访问保护。
从效果上看,互斥量起到了“唯一兵符”的作用,基于“唯一兵符”的访问保护是建立在所有访问者都遵守“约定”的基础上。也即,我们并未对公共变量本身有任何保护,我们通过遵守“约定”达成公共秩序,实现对公共变量的保护。
如果某个访问者(函数或代码段)不遵循公约,就会造成公共数据访问的混乱,引起数据错误的灾难。
以上,是关于互斥量设计的最基本的思想。实际开发中,显式地让程序员去执行mutex.lock()、mutex.unlock()是不被推崇的,如果程序员不小心没有释放mutex,会造成死锁。
为此,C++定义了 lock_guard类型,用来管理 mutex。
lock_guard 类型对象构造时以 mutex 为参数,析构时释放mutex。
怎么用 lock_guard 呢?我们在函数(或代码段)开始时定义一个 lock_guard 类型的变量,这个变量是局部的,作用域为函数体内部(或代码段内部),当函数执行结束时,该变量会自动析构,从而释放 mutex 。
但 lock_guard 实际上还有更重要的意义:当某一个使用了 lock_guard 的线程异常结束时,lock_guard 对象同样会自动析构,释放 mutex,避免别的线程永久等待(也即死锁)。
为满足各种各样的实际需求,mutex延伸出了share_mutex 等,lock_guard 则延伸出了unique_lock(手动加锁)、shared_lock、scoped_lock 等,这些延伸出的类型各有特点,开发者根据需要选用即可。
lock_guard和unique_lock的区别
- 区域锁lock_guard使用起来比较简单,除了构造函数外没有其他member function,在整个区域都有效。
- 区域锁unique_guard除了lock_guard的功能外,提供了更多的member_function,相对来说更灵活一些。
unique_guard的最有用的一组函数为:
locks the associated mutex
(public member function)tries to lock the associated mutex, returns if the mutex is not available
(public member function)attempts to lock the associated TimedLockable mutex, returns if the mutex has been unavailable for the specified time duration
(public member function)tries to lock the associated TimedLockable mutex, returns if the mutex has been unavailable until specified time point has been reached
(public member function)unlocks the associated mutex
二、示例
示例包括:
1、使用lock加锁和unlock解锁
2、使用lock_guard:用到了RAII的技术,这种技术在类的构造函数中分配资源,在析构函数中释放资源,保证资源在出了作用域之后就释放。
#include "stdafx.h"
static std::mutex g_lock;
void lock_unlock()
{
//上锁
g_lock.lock();
cout << "in id: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "out id: " << this_thread::get_id() << endl;
//解锁
g_lock.unlock();
}
void f_lock_guard()
{
//lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁.
lock_guard<std::mutex> lock(g_lock);
cout << "in id: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "out id: " << this_thread::get_id() << endl;
}
int mutex_demo()
{
std::thread t1(lock_unlock);
std::thread t2(lock_unlock);
std::thread t3(lock_unlock);
t1.join();
t2.join();
t3.join();
std::thread t4(f_lock_guard);
std::thread t5(f_lock_guard);
std::thread t6(f_lock_guard);
t4.join();
t5.join();
t6.join();
return 0;
}
参考: