通过前面三节,我们学习了线程、互斥量,乘热打铁,在这一节中我们来学习关于锁(lock)的相关知识,关于锁的声明和定义,也在头文件<mutex>中。
锁lock的类型
lock_guard | C++11 | 区域锁 |
unique_lock | C++11 | 区域锁,提供了更加灵活的操作 |
shared_lock | C++14 | 提供共享的互斥量的锁操作 |
scoped_lock | C++17 | 提供多个互斥量时避免死锁RAII的锁操作 |
很多时候我们在编写多线程代码时,在对互斥量加锁lock()后,有时可能会忘记解锁unlock(),这样就会导致该线程一直持有互斥量,而其他与其存在资源竞争的线程将会一直获取不到所有权。这完全就违背我我们利用多线程处理任务愿望。
关于lock_guard
在了解lock_guard之前,我们先来看一段代码:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 10; i++) {
lock_guard<mutex> lck(mtx); // 使用lock_guard
cout << this_thread::get_id() << "-----" << cnt++ << endl;
}
}
int main() {
thread t[3];
for (int i = 0; i < 3; i++) {
t[i] = thread(task);
}
for (int i = 0; i < 3; i++) {
t[i].join();
}
return 0;
}
看完上面的代码,可能大家会有疑问,虽然有互斥量mtx,但是并没有调用mtx对象方法:mtx.lock()或者mtx.try_lock(),也没有 出现mtx.unlock()啊。程序是怎么做到加锁、解锁的呢?
其实这就是 lock_guard<mutex> lck(mtx) 这句代码起到的功效了。好的,现在我们带着疑问来解读一下lock_guard了。
其定义如下:
template<class _Mutex>
class lock_guard { // 利用析构函数解锁互斥量
public:
typedef _Mutex mutex_type;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // ① 构造,并加锁
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { } // ② 构造,但不加锁
~lock_guard() _NOEXCEPT { // 析构函数,解锁
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
由上述定义可以知道,这是一个类模板,结构很简单,该类中定义了一个引用变量,还提供了构造函数、析构函数,禁用了拷贝构造函数和赋值函数。
lock_guard<mutex> lck(mtx);
我们再来看上面这句代码做了些什么事?显然使用的构造函数①,在初始化列表中初始化了互斥量,在函数体中对互斥量进行上锁,因此实例化一个对象的时候,便已经上锁了。
那么什么时候解锁呢?我们看到在析构函数中,对互斥量进行了解锁。也就是说,如果要解锁,那么就必须调用析构函数;我们回到上面之前的代码中分析,在for循环中,每结束一次循环,便会自动将lck析构掉,因此lock_guard的工作原理便很清晰明了了。
总的来说:lock_guard本质就是,利用程序块结束,对象被析构时进行自动解锁。这样一来,我们只要将需要进行加锁的操作(设计资源共享的代码部分)与实例化lock_guard对象放在一个语句块中即可。如:
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 1000; i++) {
{ // begin
lock_guard<mutex> lck(mtx);
cnt++;
if (cnt > 1000) cnt = 1000;
else if (cnt < 0) cnt = 0;
} // end
}
}
另外,lock_guard类中还有一个方法,在源码的注释中讲到了,该方法也是实例化一个对象,但不对互斥量上锁。主要是为了避免互斥量在已经被上锁的情况下再上锁,用法如下:
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 1000; i++) {
{ // begin
mtx.lock(); // 已上锁
lock_guard<mutex> lck(mtx, adopt_lock);
cnt++;
if (cnt > 1000) cnt = 1000;
else if (cnt < 0) cnt = 0;
} // end
}
}
其中adopt_lock是一个空类(结构体),相信读过侯JJ《STL源码剖析》的都知道(用一个空类或结构体来进行方法的重载的用法),这里也是同样的用法。其声明与定义如下:
struct adopt_lock_t { };
struct defer_lock_t { };
struct try_to_lock_t { };
extern _CRTIMP2_PURE const adopt_lock_t adopt_lock;
extern _CRTIMP2_PURE const defer_lock_t defer_lock;
extern _CRTIMP2_PURE const try_to_lock_t try_to_lock;
好了,以上就是对lock_guard的解析、用途和使用用法。
关于unique_lock
lock_guard锁非常的简单和方便,但它实在是太简洁了,而unique_lock锁为程序员提供更为灵活、强大的互斥操作、管理方法,既然是要强大一下,自然其定义就会复杂,更长一些,如下:
template<class _Mutex>
class unique_lock {
public:
typedef unique_lock<_Mutex> _Myt;
typedef _Mutex mutex_type;
// --------------------------------------构造,赋值,销毁---------------------------------------------
unique_lock() _NOEXCEPT : _Pmtx(0), _Owns(false) { } // ① 构造,空
explicit unique_lock(_Mutex& _Mtx) : _Pmtx(&_Mtx), _Owns(false) { // ② 构造,并上锁
_Pmtx->lock();
_Owns = true;
}
unique_lock(_Mutex& _Mtx, adopt_lock_t) : _Pmtx(&_Mtx), _Owns(true) { } // ③ 构造,假设已经上锁
unique_lock(_Mutex& _Mtx, defer_lock_t) _NOEXCEPT : _Pmtx(&_Mtx), _Owns(false) { } // ④ 构造,不上锁
unique_lock(_Mutex& _Mtx, try_to_lock_t) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock()) { } // ⑤ 构造,尝试上锁
// ⑥ 构造,在_Rel_time时间内尝试上锁
template<class _Rep, class _Period>
unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock_for(_Rel_time)) { }
// ⑦ 构造,在_Rel_time时间内尝试上锁
template<class _Clock, class _Duration>
unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) : _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock_until(_Abs_time)) { }
unique_lock(_Mutex& _Mtx, const xtime *_Abs_time) : _Pmtx(&_Mtx), _Owns(false) { // ⑧ 构造,在_Abs_time时间点到来之前尝试上锁
_Owns = _Pmtx->try_lock_until(_Abs_time);
}
unique_lock(unique_lock&& _Other) _NOEXCEPT : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) { // ⑨ 构造,使用move赋值
_Other._Pmtx = 0;
_Other._Owns = false;
}
// ----------------------------------------加锁,解锁-------------------------------------------
unique_lock& operator=(unique_lock&& _Other) _NOEXCEPT { // 赋值函数,使用move赋值
if (this != &_Other) { // _Other与自身对象不同(即:不是自己赋值给自己)
if (_Owns) // 已锁定
_Pmtx->unlock(); // 解锁
_Pmtx = _Other._Pmtx;
_Owns = _Other._Owns;
_Other._Pmtx = 0;
_Other._Owns = false;
}
return (*this);
}
~unique_lock() _NOEXCEPT { // 析构
if (_Owns) // 若互斥量在析构之前已经解锁了,由于存在_Owns标志,所以不会发生析构异常
_Pmtx->unlock(); // 若没有,则正常析构
}
unique_lock(const unique_lock&) = delete; // 禁用拷贝构造函数
unique_lock& operator=(const unique_lock&) = delete; // 禁用复制函数
void lock() { // 上锁
_Pmtx->lock();
_Owns = true;
}
bool try_lock() _NOEXCEPT { // 尝试上锁
_Owns = _Pmtx->try_lock();
return (_Owns);
}
template<class _Rep, class _Period>
bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) { // 在一段时间内(_Rel_time)尝试上锁
_Owns = _Pmtx->try_lock_for(_Rel_time);
return (_Owns);
}
template<class _Clock, class _Duration>
bool try_lock_until(const chrono::time_point<_Clock, _Duration>& _Abs_time) { // 尝试上锁,直到绝对时间点(_Abs_time)到来
_Owns = _Pmtx->try_lock_until(_Abs_time);
return (_Owns);
}
bool try_lock_until(const xtime *_Abs_time) { // 尝试上锁,直到绝对时间点(_Abs_time)到来
_Owns = _Pmtx->try_lock_until(_Abs_time);
return (_Owns);
}
void unlock() { // 解锁
_Pmtx->unlock();
_Owns = false;
}
// ----------------------------------------变换-------------------------------------------
void swap(unique_lock& _Other) _NOEXCEPT { //交换量unique_lock
_STD swap(_Pmtx, _Other._Pmtx);
_STD swap(_Owns, _Other._Owns);
}
_Mutex *release() _NOEXCEPT { // 断开与对象的关联(注意,调用该方法后互斥量并未解锁,所以要记得在合适的时候解锁哦)
_Mutex *_Res = _Pmtx;
_Pmtx = 0;
_Owns = false;
return (_Res); //返回unique_lock对象管理的互斥量对象的指针(下一步要解锁的话,可以用_Res->unlock())
}
// ----------------------------------------查看-------------------------------------------
bool owns_lock() const _NOEXCEPT { // 若对象持有锁,则返回true
return (_Owns);
}
explicit operator bool() const _NOEXCEPT { // 若对象持有锁,则返回true
return (_Owns);
}
_Mutex *mutex() const _NOEXCEPT { // 返回互斥量的指针
return (_Pmtx);
}
private:
_Mutex *_Pmtx; // 互斥量
bool _Owns; // 上锁标识
};
代码是有一点点长,光是构造函数就有9个(⊙o⊙)…闷一口气,我们接下来动手开始分析。
先来看是私有成员变量:一个是互斥量_Mutex,一个是标志_Owns(此标志用来表示互斥量有没有被上锁:若上锁,则为true;否则,为false)。
构造函数
① 构造一个空的unique_lock对象;
② 使用互斥量构造unique_lock对象,并对互斥量上锁(_Owns = ture);
③ 使用已上锁的互斥量构造unique_lock对象(_Owns = ture);
④ 使用互斥量构造unique_lock对象,但不对互斥量上锁(_Owns = false);
⑤ 使用互斥量构造unique_lock对象,并尝试对互斥量上锁(_Owns = _Pmtx->try_lock());
⑥ 使用互斥量构造unique_lock对象,并在_Rel_time时间内尝试对互斥量上锁(_Owns = _Pmtx->try_lock_for(_Rel_time));
⑦ 使用互斥量构造unique_lock对象,并在_Abs_time时间内尝试对互斥量上锁(_Owns = _Pmtx->try_lock_until(_Abs_time));
⑧ 使用互斥量构造unique_lock对象,并在_Abs_time时间点到来之前尝试对互斥量上锁(_Owns = _Pmtx->try_lock_until(_Abs_time));
⑨ 使用通过move(_Mutex)得到的互斥量构造unique_lock对象;
通过构造函数,我们有了大致的了解。接下来我们继续分析unique_lock是如何加上锁与解锁的。
加锁与解锁
加锁:构造函数、显式的调用lock()、try_lock()、try_lock_for()、try_lock_until()方法,这样就可以对互斥量进行加锁了;
解锁:显式的调用析构函数、调用unlock()方法;
unique_lock的变换
1、swap() —— 交换量unique_lock:_Pmtx,_Owns都需要交换(如定义);
2、release() —— 断开unique_lock对象与互斥量的关联(返回unique_lock管理的互斥量的指针,将_Owns 置0,注意此时并未解锁);
其他方法
1、unique_lock::owns_lock():判断unique_lock对象是否持有锁;
2、unique_lock::operator:判断unique_lock对象是否持有锁;
3、unique_lock::mutex():返回unique_lock对象管理的互斥量的指针;
以上便是unique_lock的主要特性,灵活使用unique_lock会给程序上带来很大的便利性。
补充
最后再补充一下之前漏掉的几个空类(结构体)的意义,这里再摘抄一遍:
struct adopt_lock_t { };
struct defer_lock_t { };
struct try_to_lock_t { };
extern _CRTIMP2_PURE const adopt_lock_t adopt_lock;
extern _CRTIMP2_PURE const defer_lock_t defer_lock;
extern _CRTIMP2_PURE const try_to_lock_t try_to_lock;
adopt_lock_t(对应adopt_lock)
作为lock_guard或unique_lock构造参数的值。adopt_lock作为参数构造锁(lock_guard或unique_lock)对象时,假设互斥量已经由当前线程加锁了,所以不会对互斥量进行上锁。
defer_lock_t(对应defer_lock)
作为lock_guard或unique_lock构造参数的值。defer_lock作为参数构造锁(lock_guard或unique_lock)对象时,不加锁也不持有锁。
try_to_lock_t(对应try_to_lock)
作为lock_guard或unique_lock构造参数的值。try_to_lock作为参数构造锁(lock_guard或unique_lock)对象时,会调用互斥量的try_lock()方法进行加锁。
若有理解失误或表达不清的地方,还请大家多多指教,谢谢~