C++(标准库):48---并发之(互斥体mutex、lock_guard、recursive_mutex、timed_mutex、recursive_timed_mutex、unique_lock)
一、mutex
- mutex全名mutual exclusion(互斥体),是个object,用来协助采取独占排他方式控制“对资源的并发访问”
- 例如,下面对一份资源进行锁定
-
void f(int val);
-
-
int val;
//共享资源
-
std::mutex valMutex;
//互斥体
-
-
void func()
-
{
-
//锁定,然后操作共享资源
-
valMutex.lock();
-
if (val >=
0)
-
f(val);
-
else
-
f(-val);
-
//访问完之后解锁
-
valMutex.unlock();
-
}
二、lock_guard
- lock_guard是采用RAII手法封装的一个类,功能与mutex一样
- 其在构造时自动对mutex进行锁定(lock),在析构时,在析构函数中自动对mutex进行解锁(unlock)
- 其比mutex的好处:
- 使用mutex,我们需要自己进行加锁(lock)和解锁(unlock)。如果对mutex进行了加锁,但是当资源访问完之后却没有对mutex进行解锁,那么其他访问这份共享资源的方法就会永远阻塞
- lock_guard的优点时在构造时自动对mutex加锁,在作用域结束/析构时,自动对mutex进行解锁
- 例如:
-
void f(int val);
-
-
int val;
-
std::mutex valMutex;
//互斥体
-
-
int main()
-
{
-
//以mutex声明一个lock_guard,其在构造时自动对传入的mutex进行lock
-
std::lock_guard<
std::mutex> lg(valMutex);
-
if (val >=
0)
-
f(val);
-
else
-
f(-val);
-
}
//作用域结束后,lg进行析构,在析构函数中,其自动对mutex进行unlock
三、mutex和lock_guard的第一个演示案例
-
#include <iostream>
-
#include <thread>
-
#include <future>
-
#include <string>
-
using
namespace
std;
-
-
std::mutex printMutex;
-
-
void print(const std::string& s)
-
{
-
//锁住mutex,保证每个线程打印时不会有别的线程也在打印
-
std::lock_guard<
std::mutex> l(printMutex);
-
for (
char c : s)
-
{
-
std::
cout.put(c);
-
}
-
std::
cout <<
std::
endl;
-
}
//作用域结束,mutex自动释放
-
-
int main()
-
{
-
auto f1 =
std::async(
std::launch::async, print,
"Hello from a first thread");
-
auto f2 =
std::async(
std::launch::async, print,
"Hello from a second thread");
-
-
print(
"Hello from the main thread");
-
}
//因为async()的发射策略为std::launch::async,即使我们没有使用future.get()获得他们的结果,但是main()一定会等待两个线程执行结束才结束
- 显示结果如下:
- 如果在print中没有使用mutex,那么显示的结果可能为:
四、递归锁(recursive_mutex)
- 在有时候,递归锁定是必要的,典型例子就是active object或monitor,它们是在每个public函数内放一个mutex并取得其lock,用以放置data race腐蚀对象的内部状态
普通的锁不能正常进行递归锁定(死锁)
- 例如,下面是一个数据库类及其接口,每个接口中都会锁定mutex成员
class DatabaseAccess { public: void createTable() { std::lock_guard< std::mutex> lg(dbMutex); //... } void insertData() { std::lock_guard< std::mutex> lg(dbMutex); //... } //这个接口中,间接调用了createTable() void createTableAndInsertData() { std::lock_guard< std::mutex> lg(dbMutex); //... createTable(); } private: std::mutex dbMutex; };
- 但是上面的createTableAndInsertData()接口会造成deadlock(死锁),因为其在内部对mutex进行加锁,之后又调用了createTable()接口,createTable()接口也会对mutex进行加锁,但是由于mutex已经被加锁了,因此createTable()发生死锁
- 如果平台侦测处类似上述的deadlock,C++标准库允许第二次lock抛出异常std::system_error并带有差错码resource_deadlock_would_occur。但并非必然而且情况往往不是如此
递归所(recursive_mutex)
- 借助recursive_mutex,上述的行为就不会有问题了。recursive_mutex允许同一线程多次锁定,并在最近一次相应的unlock时释放lock
- 例如,我们修改上面的DatabaseAccess类及其接口:
class DatabaseAccess { public: void createTable() { std::lock_guard< std::recursive_mutex> lg(dbMutex); //... } void insertData() { std::lock_guard< std::recursive_mutex> lg(dbMutex); //... } void createTableAndInsertData() { std::lock_guard< std::recursive_mutex> lg(dbMutex); //... createTable(); } private: std::recursive_mutex dbMutex; };
五、mutex的成员函数:尝试性的lock(try_lock())
- try_lock()成员函数的作用是:对mutex进行锁定,如果能锁定就返回true,如果不能锁定就不阻塞直接返回false
-
std::mutex m;
-
-
//对m进行尝试性加锁,加锁成功才结束while
-
while (m.try_lock() ==
false)
-
{
-
doSomeOtherStuff();
-
}
-
-
//...
-
-
//使用完解锁
-
m.unlock();
- 如果lock_guard想要使用try_lock()加的锁,需要传递一个额外实参adopt_lock给其构造函数。例如:
-
std::mutex m;
-
-
//对m进行尝试性加锁,加锁成功才结束while
-
while (m.try_lock() ==
false)
-
{
-
doSomeOtherStuff();
-
}
-
//加锁完成之后,将mutex交给lock_guard<>进行管理,此时需要传入std::adopt_lock参数
-
std::lock_guard<
std::mutex> lg(m,
std::adopt_lock);
- 注意:try_lock()有可能假性失败,也就是说即使lock并未被他人使用也可能失败返回false(之所以提供这样的行为是为了memory-ordering(内存处理次序),但是这个并不广为人知)
六、带时间性的lock(timed_mutex、recursive_timed_mutex)
- 为了等待特定的时间再进行加锁,那么可以使用所谓timed mutex
- 标准库提供了std::timed_mutex和std::recursive_timed_mutex。并且这两个类都提供了try_lock_for()和try_lock_until(),用以等待某个时间段,或直至到达某个时间点再进行加锁
- 例如:
-
std::timed_mutex m;
-
-
//尝试加锁1秒钟,如果在1秒钟加锁成功,就执行if
-
if (m.try_lock_for(
std::chrono::seconds(
1)))
-
{
-
//将timed_mutex交给lock_guard进行管理,注意其构造函数需要传入std::adopt_lock
-
std::lock_guard<
std::timed_mutex> lg(m,
std::adopt_lock);
-
}
-
else
-
{
-
couldNotGetTheLock();
-
}
- 注意,处理系统时间调整时,try_lock_for()和try_lock_until往往有异(详情见此篇文章中的“七”:https://blog.csdn.net/qq_41453285/article/details/105464872)
七、处理多个lock(全局std::lock()函数)
- 有时候,一次需要锁定多个mutex(例如为了传送数据,从一个受保护资源到另一个受保护的资源,需要将两份资源同时锁定)
- 如果使用前面的lock机制,那么可能会发生复杂且具有风险:例如你取得第一个lock却拿不到第二个lock,或许发生deadlock(如果以不同的次序去锁住相同的lock)
全局std::lock()函数
- 全局std::lock()函数允许你一次锁定多个mutex
- 例如:
std::mutex m1; std::mutex m2; void func() { std::lock(m1, m2); std::lock_guard< std::mutex> lockM1(m1, std::adopt_lock); std::lock_guard< std::mutex> lockM2(m2, std::adopt_lock); //... } //作用域结束后,m1,m2都自动释放
- std::lock()的注意事项:
- 该函数会锁住它收到的所有mutex,并且阻塞到所有的mutex都被锁定或直到抛出异常才返回
- 如果锁定的过程中抛出了异常,那么已经加锁的mutex会被释放
- 在锁定之后,你可以配合lock_guard对mutex进行使用,并且需要传递std::adopt_lock给lock_guard的构造函数
- 这个lock()函数提供了一个deadlock回避机制,但是多个lock的锁定次序并不明确
全局std::try_lock()函数
- 该函数也可以对多个mutex进行加锁,但是其实进行尝试性加锁的。其工作原理与返回值密切相关,详情见下面的返回值
- 返回值:
- 如果对所有的mutex都加锁成功,返回-1。此时可以对所有的mutex进行操作了
- 如果对其中的一部分锁没有加锁成功,那么返回第一个失败的lock的索引(从0开始)。此时已经成功加锁的mutex会被释放
- 例如:
std::mutex m1; std::mutex m2; void func() { int idx = std::try_lock(m1, m2); //如果对m1和m2都加锁成功,返回-1,执行if if (idx < 0) { std::lock_guard< std::mutex> lockM1(m1, std::adopt_lock); std::lock_guard< std::mutex> lockM2(m2, std::adopt_lock); } else { std:: cerr << "could not lock mutex m" << idx + 1 << std:: endl; } }
- 注意:这个try_lock()不提供deadlock会比机制,但它保证以出现于try_lock()实参列的次序来对mutex进行尝试性加锁
- 注意:使用lock()函数或try_lock()函数对mutex进行加锁之后,你仍然需要在作用域结束之后对mutex进行解锁(unlock()),但是一般我们会将其传递给lock_guard<>使用
八、unique_lock
- 标准库还提供了一个unique_lock<>类,它对付mutex更有弹性
- unique_lock<>的接口和lock_guard<>的接口相同,但是允许写出“何时加锁”以及“如何锁定或解锁”其mutex。此外,unique_lock还提供了owns_lock()和bool()接口来查询其mutex目前是否被锁住
三种锁定标志
- 第一种:你可以传递try_to_lock,表示尝试性对mutex进行加锁,如果加锁成功就返回true,如果加锁失败就不阻塞而直接返回false。代码如下:
std::mutex m; void func() { //尝试加锁m,但是不阻塞 std::unique_lock< std::mutex> lock(m, std::try_to_lock); //如果加锁成功,执行if if (lock) { //... } } //作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m;如果没有,其析构函数什么都不做
- 第二种:你可以传递一个时间段或时间点给构造函数,表示尝试在一个明确的时间上对mutex进行加锁
std::mutex m; void func() { //在1秒钟之后,对m进行加锁,如果加锁成功就返回,加锁失败就阻塞等待 std::unique_lock< std::mutex> lock(m, std::chrono::seconds( 1)); //... } //作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m;如果没有,其析构函数什么都不做
- 第三种:你可以传递defer_lock给其构造函数,表示初始化unique_lock,但是并不加锁,而是在后面自己调用lock()成员函数进行加锁
std::mutex m; void func() { //只是单单的用m初始化lock,但是不进行加锁 std::unique_lock< std::mutex> lock(m, std::defer_lock); //... lock.lock(); //调用此成员函数,对m进行加锁 //... } //作用域结束后,如果unique_lock对m成功加锁,那么unique_lock的析构函数释放m(不需要调用unlock);如果没有,其析构函数什么都不做
std:::defer_lock标志
- 上述的第三种使用方式中的std:::defer_lock标志,可以用来建立一或多个lock并于稍后才锁住它们
- 例如:
std::mutex m1; std::mutex m2; std::mutex m3; void func() { std::unique_lock< std::mutex> lockM1(m1, std::defer_lock); std::unique_lock< std::mutex> lockM2(m2, std::defer_lock); std::unique_lock< std::mutex> lockM3(m3, std::defer_lock); //... std::lock(m1, m2, m3); }
- 如果没有任何锁定标志,那么unique_lock与lock_guard一样,直接对mutex进行锁定
-
std::mutex m;
-
-
void func()
-
{
-
//与lock_guard<>一样,尝试对m进行加锁,加锁成功就返回;加锁失败就阻塞
-
std::unique_lock<
std::mutex> lock(m);
-
}
- 此外,unique_lock还提供了release()用来释放mutex,或是将其mutex拥有权转移给另一个lock。详细的成员函数见下面“九”中的介绍
演示案例
- 有了lock_guard和unique_lock作为工具,现在我们可以实现一个例子,以轮询某个ready flag的方式,令一个线程等待另一个线程
- 代码如下:
bool readyFlag; std::mutex readyFlagMutex; void thread1() { //做一些thread2需要的准备工作 //... std::lock_guard< std::mutex> lg(readyFlagMutex); readyFlag = true; } void thread2() { //等待readyFlag变为true { std::unique_lock< std::mutex> ul(readyFlagMutex); //如果readyFlag仍未false,说明thread1还没有锁定,那么持续等待 while (!readyFlag) { ul.unlock(); std::this_thread::yield(); std:this_thread::sleep_for( std::chrono::milliseconds( 100)); ul.lock(); } } //释放lock //在thread1锁定之后,做相应的事情 }
九、细说mutex和lock
细说mutex
- 标准库提供了四个mutex,如下:
- 比较如下:
- 下面列出了mutex的操作函数:
- lock()成员函数有可能抛出std::system_error并带有下面的差错码:
- operation_not_permitted——如果线程的特权级(privilege)不足以执行次操作
- resource_deadlock_would_occur——如果平台侦测到有个deadlock即将发生
- device_or_resource_busy——如果mutex已被锁定而无法形成阻塞(blocking)
- 如果程序解除(unlock)一个并非它所拥有的mutex object,或是销毁一个被任何线程拥有的mutex object,或是线程拥有mutex object但却结束了生命,将导致不明确的行为
- 注意,处理系统时间调整时,try_lock_for()和try_lock_until()通常有异
细说lock_guard
- 下图列出了lock_guard的操作函数
细说unique_lock
- unique_lock为一个不一定得锁住的mutex提供一个lock guard。它提供的接口如下图所示:
- lock()可能抛出std::system_error,其所夹带的差错码和mutex的lock()所引发的相同
- unlock()可能抛出std::system_error并夹带差错码operation_not_permitted——如果这个unique lock并未被锁的话
转载:https://blog.csdn.net/qq_41453285/article/details/105602105
查看评论