多线程 - 线程锁
多线程 - 线程锁
互斥量(mutex)
简概
简概
- 互斥量是个类对象,理解成一把锁
- 使用互斥量要小心,多了影响效率、少了则不安全
使用
std::mutex mymutex;
,创建类成员变量lock()
,成员函数能够对线程进行加锁。同一时间只有一个线程能够锁成功; 锁成功时会返回true,若没锁成功则阻塞会一直尝试调用lock()unlock()
,成员函数能够对线程进行解锁- 使用步骤:lock -> 操作共享数据 -> unlock,其中lock和unlock要成对使用
智能互斥量
std::lock_guard
类模板,能自动unlock(防止你忘记unlock、有点像智能指针自动释放内存)- 原理:
std::lock_guard<std::mutex> sbguard(my_mutex);
生成的互斥量对象,在构造函数和析构函数调用时,自动加锁和解锁 是对局部变量的巧妙运用 - 缺点:不太灵活,无法自由控制解锁的位置,不能手动unlock。但可以配合
{}
来提高灵活度
错误例程(边读边写,会崩溃)
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
class A{
public:
// 收集数据的线程
void inMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
cout << "_";
msgRecvQueue.push_back(i); // 写操作是非原子性的
}
}
// 取出数据的线程
void outMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
if(!msgRecvQueue.empty())
{
msgRecvQueue.front(); // 返回第一个元素但不检查是否存在
msgRecvQueue.pop_front(); // 移除第一个元素但不返回,写操作是非原子性的
cout << "O";
}
else
{
cout << "空";
}
}
}
private
std::list<int> msgRecvQueue; // 收到的消息列表
}
int main(){
A a;
thread threadI(&A::inMsgRecQueue, &a); // 成员函数作为线程初始函数
thread threadO(&A::outMsgRecQueue, &a); // 成员函数作为线程初始函数
threadI.json();
threadO.json();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
加锁例程(lock/unlock)
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
class A
{
public:
// 收集数据的线程
void inMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
cout << "_";
mymutex.lock(); // 【互斥量】加锁
msgRecvQueue.push_back(i); // 写操作是非原子性的
mymutex.unlock(); // 【互斥量】解锁
}
}
// 取出数据的线程
void outMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
mymutex.lock(); // 【互斥量】加锁
if(!msgRecvQueue.empty()) // 读
{
mymutex.lock(); // 【互斥量】加锁
msgRecvQueue.front(); // 返回第一个元素但不检查是否存在
msgRecvQueue.pop_front(); // 移除第一个元素但不返回,写操作是非原子性的
cout << "O";
}
mymutex.unlock(); // 【互斥量】解锁
else
{
cout << "空";
}
}
}
private
std::list<int> msgRecvQueue; // 收到的消息列表
std::mutex mymutex; // 【互斥量】创建一个互斥量成员变量
}
int main()
{
A a;
thread threadI(&A::inMsgRecQueue, &a); // 成员函数作为线程初始函数
thread threadO(&A::outMsgRecQueue, &a); // 成员函数作为线程初始函数
threadI.json();
threadO.json();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
智能互斥量
lock_guard
lock_guard
加锁例程
原理:std::lock_guard<std::mutex> sbguard(my_mutex);
生成的互斥量对象,在构造函数和析构函数调用时,自动加锁和解锁
class A
{
public:
// 收集数据的线程
void inMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
cout << "_";
std::lock_guard<std::mutex> sbguard(my_mutex); // 【智能互斥量】
msgRecvQueue.push_back(i); // 写操作是非原子性的
}
}
// 取出数据的线程
void outMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
if(!msgRecvQueue.empty()) // 读
{
std::lock_guard<std::mutex> sbguard(my_mutex); // 【智能互斥量】
msgRecvQueue.front(); // 返回第一个元素但不检查是否存在
msgRecvQueue.pop_front(); // 移除第一个元素但不返回,写操作是非原子性的
cout << "O";
}
else
{
cout << "空";
}
}
}
private
std::list<int> msgRecvQueue; // 收到的消息列表
std::mutex mymutex; // 【互斥量】创建一个互斥量成员变量
}
lock_guard 第二个参数
第二个参数:该参数是一个结构体对象,起一个标记的作用
std::adopt_lock
作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了
应用:创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);
unique_lock
unique_lock
- 简概
unique_lock
是个类模板。和lock_guard
的作用类似,但更灵活
- 比较unique_lock、lock_guard
- 选用:一般选用lock_guard
- 优点:更灵活。可以在代码中自由加锁解锁(锁锁住代码的多少,称为锁的
粒度的粗细
,粒度越细执行效率越高) - 缺点:效率低一点,内存占用多点,但差别不大
- 使用
- 可直接替换lock_guard使用
- 此时,功能和lock_guard没什么区别
#include <iostream>
#include <thread> // 线程头文件
using namespace std;
class A
{
public:
// 收集数据的线程
void inMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
cout << "_";
std::unique_lock<std::mutex> sbguard(my_mutex); // 【unique_lock】
msgRecvQueue.push_back(i); // 写操作是非原子性的
}
}
// 取出数据的线程
void outMsgRecQueue()
{
for(int i=0; i<10000; ++i) // 增加崩溃几率
{
std::unique_lock<std::mutex> sbguard(my_mutex); // 【unique_lock】
if(!msgRecvQueue.empty()) // 读
{
msgRecvQueue.front(); // 返回第一个元素但不检查是否存在
msgRecvQueue.pop_front(); // 移除第一个元素但不返回,写操作是非原子性的
cout << "O";
}
else
{
cout << "空";
}
}
}
private
std::list<int> msgRecvQueue; // 收到的消息列表
std::mutex mymutex; // 【互斥量】创建一个互斥量成员变量
}
int main()
{
A a;
thread threadI(&A::inMsgRecQueue, &a); // 成员函数作为线程初始函数
thread threadO(&A::outMsgRecQueue, &a); // 成员函数作为线程初始函数
threadI.json();
threadO.json();
cout << "《主线程》" << endl
<<"《线程id》"<<std::this_thread:get_id()<<endl;
return 0;
}
unique_lock 休眠操作
休眠(锁完以后休眠20s)
std::unique_lock<std::mutex> sbguard(my_mutex); // 【unique_lock】
std::chrono::milliseconds dura(2000); // 【20s】
std::this_thread::sleep_for(dura); // 【休息20s】
unique_lock 第二个参数
第二个参数:该参数是一个结构体对象,起一个标记的作用
std::adopt_lock
作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了 使用前需要手动lock互斥量
应用:创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::unique_lock<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::unique_lock<std::mutex> sbguard(mutexB, std::adopt_lock);
这个标记和在lock_guard中的用法一样
std::try_to_lock
作用:尝试去lock(),若没有锁定成功则立即返回,而不会阻塞 注意使用前不要自己先lock互斥量,否则会出错
应用:
std::unique_lock<std::mutex> sbgruad1(my_mutex1, std::try_to_back); if(shguard1.owns_lock()) // 拿到了锁 { // 操作共享数据 } else // 没拿到锁 { // 干点其他事情 }
std::defer_lock
作用:初始化一个没有加锁的mutex 使用前不能自己lock互斥量,否则报异常 使用后需要lock(),且不需要手动解锁
优点:加锁解锁较自由
缺点:这里并没有真正加锁原来的那个互斥量,读写时可能会出问题(不稳定)
应用:
std::unique_lock<std::mutex> sbgruad1(my_mutex1, std::defer_lock); // 初始化一个没有加锁的mutex并添加到智能指针的成员 subgruad1.lock(); // 加锁那个初始化的mutex,并且该mutex可以被自动解锁 // 处理一些非共享数据 subgruad1.unlock(); subgruad1.lock(); // 继续处理共享数据 // subgruad1.unlock(); // 写不写都行,该模式的智能互斥量指针析构会自动检查有无解锁
unique_lock 成员函数
lock()
- 加锁,和互斥量用法一样
unlock()
- 解锁,和互斥量用法一样
try_lock()
- 尝试给互斥量加锁,如果拿不到锁则返回false,否则返回true。不阻塞
- 和unique_lock的
std::try_to_lock
参数的作用类似
release()
返回它管理的mutext对象指针,并释放所有权
即使unique_lock和构建它的mutext不再有关系
std::unique_lock<std::mutex> sbguard1(my_mutex1); std::mutex *ptx = sbguard1.release(); // 此时 ptx == my_mutex1。由于和智能互斥量解除关系,现在需要自己解锁了 // ... ptx->unlock();
unique_lock 所有权的传递
概念
如下例程所示
std::unique_lock<std::mutex> sbguard1(mutex1); // 此时sbguard1拥有mutex1的所有权 std::unique_lock<std::mutex> sbguard2(mutex1); // 此时sbguard2不能再拥有mutex1的所有权,该行会报错
所有权的转移
概念:unique_lock该对象可以转移mutex参数的所有权,但不能复制。这和unique_ptr一样,属于独占型智能指针
方法1(std::move方法调用移动构造函数)
std::unique_lock<std::mutex> sbguard1(mutex1); // 此时sbguard1拥有mutex1的所有权 // std::unique_lock<std::mutex> sbguard2(sbguard1); // 非法报错,不能复制所有权 std::unique_lock<std::mutex> sbguard2(std:move(sbguard1)); // 移动语义,参数变为右值,所有权转移。此时sbguard1指向空
方法2(返回局部对象从而调用移动构造函数)
std::unique_lock<std::mutex> rtn_unique_lock(){ std::unique_lockKstd::mutex> tmpguard(my_mutex1): return tmpguard: //从函数返回一个局部的unique_lock对象是可以的。 // 移动构造函数的知识:返回局部对象会导致系统生成临时的对象,并调用相应的移动构造函数 } std::unique_lock<std::mutex> sbguard1 = rtn_unique_lock();
死锁(多个互斥量)
造成原因:至少有两个互斥量才会造成死锁
- 抽象场景举例
- 线程1依次执行:
mutexA.lock();
、mutexB.lock();
- 线程2依次执行:
mutexB.lock();
、mutexA.lock();
- 当线程1锁完mutexA后上下文切换为线程2并锁了mutexB,就会形成死锁
- 此时两个线程都会一直阻塞
- 线程1依次执行:
- 具体场景距离
- 占有资源、并去抢占其他资源,就有可能会出现死锁
- 抽象场景举例
解决方案
- 保证两个互斥量上锁的顺序一致
- 可用
std::lock()
函数模板解决
std::lock()
函数模板- 作用:一次锁住多个互斥量,可避免在多个线程中因为锁顺序问题导致死锁
- 原理:如遇到没被锁的互斥量则锁住,直到互斥量中有一个没锁住则立即解锁之前锁住的互斥量,并阻塞循环直到组合中所有互斥量都被锁住
- 使用:
std::lock(mutexA, mutexB);
后,要与解锁配对:mutexA.unlock(); mutexB.unlock();
结合智能互斥量
std::adopt_lock
参数- 简概:这是个结构体对象,起一个标记的作用
- 作用:表示这个互斥量已经lock()了,不需要再lock一遍,只要负责析构时解锁就行了
- 应用:创建智能互斥量对象(组合)
写法
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);
总结
std::mutex
,创建互斥量对象std::lock()
,组合锁的函数模板创建智能互斥量对象(非组合)
std::lock_guard<std::mutex> sbguard(mutexA);
创建智能互斥量对象(组合)
std::lock(mutexA, mutexB); // 先组合锁 std::lock_guard<std::mutex> sbguard(mutexA, std::adopt_lock); // 再设为析构自动解锁 std::lock_guard<std::mutex> sbguard(mutexB, std::adopt_lock);