C++11:互斥锁std::mutex和std::lock_guard/std::unique_lock

Posted 木大白易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11:互斥锁std::mutex和std::lock_guard/std::unique_lock相关的知识,希望对你有一定的参考价值。

简介

C++11中新增了std::mutex,它是C++标准程序库中的一个头文件,定义了C++11标准中的一些互斥访问的类与方法等。

mutexmutual exclusion(互斥)的简写。
共有4个类可以使用:

API说明
mutex提供基本互斥设施
timed_mutex提供互斥设施,带有超时功能
recursive_mutex提供能被同一线程递归锁定的互斥设施
recursive_timed_mutex提供能被同一线程递归锁定的互斥设施,带有超时功能

mutex的函数成员

访问共享数据的代码片段称之为临界区(critical section)

我们现在先只关注最基本的mutexmutex是最基础的API。其他类都是在它的基础上的改进。所以这些类都提供了下面三个方法,并且它们的功能是一样的:

方法说明
lock()锁定互斥体,如果不可用,则阻塞
try_lock()尝试锁定互斥体,如果不可用,直接返回
unlock ()解锁互斥体

这三个方法提供了基础的锁定解除锁定的功能。使用lock意味着你有很强的意愿一定要获取到互斥体,而使用try_lock则是进行一次尝试。这意味着如果失败了,你通常还有其他的路径可以走。

所以最基本的使用:

std::mutex mtx;

void someOp()
	mtx.lock(); //加锁
	... //执行你的操作,这块是临界区
	mtx.unlock(); //解锁

在进入临界区时,执行lock()加锁操作,如果这时已经被其它线程锁住,则当前线程在此排队等待。退出临界区时,执行unlock()解锁操作。

通用互斥管理

互斥体(mutex相关类)提供了对于资源的保护功能,但是手动的锁定(调用lock或者try_lock)和解锁(调用unlock)互斥体是要耗费比较大的精力的。因为我们需要保证,在任何情况下,解锁要和加锁配对,因为假设出现一条路径导致获取锁之后没有正常释放(比如在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题),就会影响整个系统。如果考虑方法还可以会抛出异常,这样的代码写起来会很费劲。

更好的办法是采用“资源分配时初始化(RAII)”方法来加锁、解锁,标准库就提供了下面的这些API,来简化我们手动加锁和解锁的“体力活”。

API说明
lock_guard实现严格基于作用域的互斥体所有权包装器
unique_lock实现可移动的互斥体所有权包装器
锁定策略说明
defer_lock类型为 defer_lock_t,不获得互斥的所有权
try_to_lock类型为try_to_lock_t,尝试获得互斥的所有权而不阻塞
adopt_lock类型为adopt_lock_t,假设调用方已拥有互斥的所有权

lock_guard

所以上边的代码可以变成这样:

std::mutex mtx;

void someOp()
	std::lock_guard<std::mutex> lock(mtx);; //加锁
	... //执行你的操作,这块是临界区

//出了作用域,自动释放锁mtx

std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。

unique_lock

unique_lock具有lock_guard的全部功能,但是更加灵活:

  1. lock_guard在构造时或者构造前(std::adopt_lock)就已经获取互斥锁,并且在作用域内保持获取锁的状态,直到作用域结束;而unique_lock在构造时或者构造后(std::defer_lock)获取锁,在作用域范围内可以手动获取锁和释放锁,作用域结束时如果已经获取锁则自动释放锁。
  2. lock_guard锁的持有只能在lock_guard对象的作用域范围内,作用域范围之外锁被释放,而unique_lock对象支持移动操作,可以将unique_lock对象通过函数返回值返回,这样锁就转移到外部unique_lock对象中,延长锁的持有时间。

所以上边代码也可以这样:

std::mutex mtx;

void someOp()
	std::unique_lock<std::mutex> lock(mtx, std::defer_lock);; //此时还未加锁
	lock.lock();//手动获取锁
	... //执行你的操作,这块是临界区
	lock.unlock();//手动释放锁

另外和条件变量condition_variable一起使用的时候,也需要用unique_lock

推荐阅读

C++ 并发编程(从C++11到C++17)

以上是关于C++11:互斥锁std::mutex和std::lock_guard/std::unique_lock的主要内容,如果未能解决你的问题,请参考以下文章

[C++11 多线程同步] --- 互斥锁

[C++11 多线程同步] --- 互斥锁

[C++11 多线程同步] --- 互斥锁

确保当前线程持有 C++11 互斥锁

C11线程管理:互斥锁

C++11 并发指南三(std::mutex 详解)