上一节讲解了四种锁,但这四种锁并不符合C++ RAII的要求,因此C++11引入了lock_guard和unique_lock两个类模板。
RAII(Resource acquisition is initialization ):也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。 ——百度百科
本篇相比之前内容更多,加入了源码的阅读和众多函数,请耐心阅读
Lock_guard是一个类模板。
#include
mutex m_lock;//创建锁
lock_guard<mutex> g_lock(m_lock);//使用g_lock绑定m_lock
在一个作用域内创建lock_guard对象时,会尝试获得锁(mutex::lock()),没有获得就像其他锁一样阻塞在原地。
在lock_guard的析构函数内会释放锁(mutex::unlock()),就不需要我们手动释放。
在实际编程中建议使用lock_guard,防止在获得锁后程序运行出现异常退出而导致锁死。
注意在我们使用lock_guard时就不要手动unlock()了,不然lock_guard的析构函数中解锁时就会报异常。
#include
#include
#include
using namespace std;
class Test
{
public:
Test();
~Test();
void f() {
lock_guard<mutex> lg(lx);
cout << "f()" << endl;
}
void f1() {
lock_guard<mutex> lg(lx);
cout << "f1()" << endl;
}
private:
mutex lx;
};
Test::Test()
{
}
Test::~Test()
{
};
int main() {
Test T;
while (1) {
thread t1(&Test::f, &T);
thread t2(&Test::f1, &T);
thread t3(&Test::f1, &T);
thread t4(&Test::f1, &T);
t1.join();
t2.join();
t3.join();
t4.join();
}
return 0;
}
在今后的讲解中我都会尝试去翻阅一下源码和相关资料,毕竟源码的阅读对我们用于构造类和理解mutex也有很好的帮助
lock_guard在创建还可以传入第二个参数:adopt_lock;
这里给出lock_guard构造函数源码
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
// construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{
// construct but don't lock
}
从两个构造函数源码中我们可以清晰的看到,传入adopt_lock是表明程序不需要自动帮我们对传给lock_gurad的锁进行lock()操作。因此,如果我们使用adopt_lock的话我们是需要进行手动上锁的。
_MyMutex是一个_Mutex的引用,_Mutex就是<>里的类型参数,无法理解的话你就把他当做 mutex。对源码还有疑问的小伙伴可以自行查阅相关资料,这里不做过多解释
void Test::f1() {
lock_guard<mutex> lg(lx,adopt_lock);
lx.lock(); //如果不调用lock(),在运行中就会报异常
cout << "f1()" << endl;
}
unique_lock也是一个类模板,和lock_guard是差不多的,也具有构造函数中加锁,析构函数解锁的能力,符合RAII原则。
但区别在于unique_lock提供了我们解锁能力(unique_lock::lock(),unique_lock::unlock() , 注意区分unique_lock对象的lock()方法,而不是mutex::lock(),虽然功能一样,但要理解调用对象的不同),可由程序员来进行手动解锁,这和lock_guard是不一样。unique_lock的极高灵活性提供了我们优化代码的能力,因为一旦lock_guard在创建获得锁之后必须要等到析构时才会释放锁,这样就造成其他线程在执行不涉及临界资源的代码时浪费了时间,大大降低效率。但这也代表着它的效率更低,占用内存更大(sizeof(lock_guard)是4,sizeof(unique_lock)是8,原因是多了一个_Owns变量,这是一个bool值,具体用途稍后介绍)。
class Test
{
public:
Test();
~Test();
void f() {
unique_lock<mutex> lg(lx);
cout << "f()" << endl;
}
void f1() {
unique_lock<mutex> lg(lx);
cout << "f1()" << endl;
lg.unlock(); //这里我们调用unlock()的话是不会像lock_guard那样报错的。
lg.lock();
//dosomething();
lg.unlock();//别忘记unlock
}
private:
mutex lx;
};
Test::Test()
{
}
Test::~Test()
{
};
int main() {
Test T;
while (1) {
thread t1(&Test::f, &T);
thread t2(&Test::f1, &T);
thread t3(&Test::f1, &T);
thread t4(&Test::f1, &T);
t1.join();
t2.join();
t3.join();
t4.join();
}
return 0;
}
可能讲到这里,明白unique(唯一的)这个单词中文意思的同学还不能了解“唯一”在哪。
这里我们给出 =运算符 源码:
unique_lock& operator=(unique_lock&& _Other)//注意这里是右值
{
// destructive copy
if (this != &_Other)
{
// different, move contents
if (_Owns)
_Pmtx->unlock();
_Pmtx = _Other._Pmtx;
_Owns = _Other._Owns;
_Other._Pmtx = 0;
_Other._Owns = false;
}
return (*this);
}
_Owns就是刚刚提到使 unique_lock<>占用空间更大的bool变量,用以表示是否持有锁
__Pmtx是一个_Mutex类型的指针
_Mutex就是<>里的类型参数,无法理解的话你就把他当成mutex类型
从源码里我们看出,一旦用 =运算将一个 unique_lock 赋值给另一个unique_lock后,原先的锁就会被释放,这里和unique_ptr有点像。值得注意的是,之前我们提过的四种mutex变量都没有重载等号运算符和拷贝构造函数,因为他们都继承于_Mutex_base这个类。
unique_lock 和 lock_guard 一样 可以传入第二个参数:
表明我们需要手动上锁,具体看之前代码
这里给出源码
unique_lock(_Mutex& _Mtx, adopt_lock_t)
: _Pmtx(&_Mtx), _Owns(true)
{
// construct and assume already locked
}
还有其他参数也可以传入
初始化时并不锁住lx
void f1() {
unique_lock<mutex> lg(lx,defer_lock);
dosomething();
}
这里给出源码
unique_lock(_Mutex& _Mtx, defer_lock_t) _NOEXCEPT
: _Pmtx(&_Mtx), _Owns(false)
{ // construct but don't lock
}
尝试去获得锁
这里给出源码
unique_lock(_Mutex& _Mtx, try_to_lock_t)
: _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock())
{
// construct and try to lock
}
这里可以从初始化列表看出_Owns是由传入的 _Mtx来决定值,之前也提到过try_lock(),有疑问记得回顾
bool owns_lock(): 返回_Owns 也就是是否用有锁
mutex* release():返回类中的锁指针,代表调用这个方法的unique_lock和传给它的mutex就没有任何关系了,需要我们之后手动进行mutex::lock()以及
mutex::unlock()操作。
operatpr bool(): 返回_Owns,和owns_lock()一样
unique_lock(unique_lock&& _Other) :传入另一个unique_lock的构造函数,注意这是一个破坏性拷贝,他会使传进来的 _Other恢复默认状态。
后面讲解较少的这部分是不怎么常用的,其实unique还有好多方法和第二参数,但因使用较少就不多做讲述,防止记忆混乱
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
——百度百科
互斥: 资源单独访问。
占有且等待: 本身占有资源但还未满足启动条件,保持当前资源并等待其他资源。
不可抢占: 无法获得还未释放的资源。
循环等待: 每个进程都在等待别人手上的资源,并且形成回路。
当以上四个条件均满足,就会造成死锁,发生死锁的进程无法进行,所持有的资源也无法释放。
死锁情况非常浪费系统资源和影响计算机的使用性能的,因此我们在多线程编程中也需要注意防止死锁。
有关扩展自行查找操作系统系列知识
std::lock()函数:
注意这里的lock()函数并不是mutex::lock(),也不是unique::lock()。
//假设我们某个线程需要两个资源A,B 我们用lock_A和lock_B来锁住他们。
//不用①lock_A::lock();②lock_B::lock();的原因是可能我们在某个线程执行①的时候,
//另一个线程执行了②,就会发生死锁
//我们可以使用std::lock()函数来保证同时拿到两种资源。
void f1(){
lock(lock_A,lock_B);
lock_guard<mutex> lockA(lock_A, adopt_lock); //这里就可以使用adopt_lock了 不给出原因 ,自行思考加深印象
lock_guard<mutex> lockB(lock_B, adopt_lock);
}
本文仅用于本人学习过程中类似于笔记的记录,如有疑问和错误请在评论区指出。
请勿转载,谢谢您的观看。