C++11多线程(三) lock_guard unique_lock

文章目录

  • C++11多线程(三) lock_guard unique_lock
    • 导读
    • Lock_guard
      • 示例代码
      • lock_guard<>的第二个参数
    • unique_lock
      • unique_lock源码浅析(部分)
      • unique_lock的第二个参数
        • adopt_lock
        • defer_lock
        • try_to_lock
      • unique_lock常用函数以及一些补充
    • 死锁
      • 死锁的概念
      • 死锁产生的4个必要条件
      • C++11中防止死锁的方法

C++11多线程(三) lock_guard unique_lock

导读

上一节讲解了四种锁,但这四种锁并不符合C++ RAII的要求,因此C++11引入了lock_guard和unique_lock两个类模板。

RAII(Resource acquisition is initialization ):也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。 ——百度百科

本篇相比之前内容更多,加入了源码的阅读和众多函数,请耐心阅读

Lock_guard

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;
}

lock_guard<>的第二个参数


在今后的讲解中我都会尝试去翻阅一下源码和相关资料,毕竟源码的阅读对我们用于构造类和理解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

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_lock源码浅析(部分)

可能讲到这里,明白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的第二个参数

unique_lock 和 lock_guard 一样 可以传入第二个参数:

adopt_lock

表明我们需要手动上锁,具体看之前代码

这里给出源码

unique_lock(_Mutex& _Mtx, adopt_lock_t)
		: _Pmtx(&_Mtx), _Owns(true)
		{
     	// construct and assume already locked
		}

还有其他参数也可以传入

defer_lock

初始化时并不锁住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
		}

try_to_lock

尝试去获得锁

这里给出源码

unique_lock(_Mutex& _Mtx, try_to_lock_t)
		: _Pmtx(&_Mtx), _Owns(_Pmtx->try_lock())
		{
     	// construct and try to lock
		}

这里可以从初始化列表看出_Owns是由传入的 _Mtx来决定值,之前也提到过try_lock(),有疑问记得回顾

unique_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还有好多方法和第二参数,但因使用较少就不多做讲述,防止记忆混乱

死锁

死锁的概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

——百度百科

死锁产生的4个必要条件

  1. 互斥: 资源单独访问。

  2. 占有且等待: 本身占有资源但还未满足启动条件,保持当前资源并等待其他资源。

  3. 不可抢占: 无法获得还未释放的资源。

  4. 循环等待: 每个进程都在等待别人手上的资源,并且形成回路。
    当以上四个条件均满足,就会造成死锁,发生死锁的进程无法进行,所持有的资源也无法释放。

死锁情况非常浪费系统资源和影响计算机的使用性能的,因此我们在多线程编程中也需要注意防止死锁。

有关扩展自行查找操作系统系列知识

C++11中防止死锁的方法

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);

}

本文仅用于本人学习过程中类似于笔记的记录,如有疑问和错误请在评论区指出。
请勿转载,谢谢您的观看。

你可能感兴趣的:(c++thread,c++,多线程,c++11)