文章目录
一、互斥量(mutex)概念的基本概念
- 互斥量是一个类对象,理解成一把锁,多个线程尝试用lock()成员函数来尝试加锁头,只有一个线程能够锁定成功,成功的标志是返回,如果没有所成功,那么这个线程的执行流程就会卡在lock()这里不断尝试去锁这把锁;
- 互斥量使用需要小心:只保护需要保护的数据,也必须保护全(保护多了影响效率,保护少了达不到保护效果),操作完以后要把锁解开,别人才能使用lock继续执行;
二、互斥量的用法
1.lock(),unlock()
- 步骤:先lock(),操作共享数据,再unlock();
- lock()和unlock()要成对使用,每调用一次lock(),必须调用一次unlock();
- 有lock()忘记unlock()一般很难排查;
- 为了防止大家忘记unlock(),引入了一个叫std::lock_guard()的类模板;你忘记unlock(),我帮你unlock();
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
//如果没有锁,会不定时崩溃
my_mutex.lock();
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
bool outMsgLULProc(int &command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex.unlock();
return true;
}
my_mutex.unlock();
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
//容器:专用于代表玩家发送过来的命令
list<int> msgRecvQueue;
std::mutex my_mutex; //创建一个互斥量
};
int main()
{
//数据共享:网络游戏服务器
//有两个自己创建的线程
//一个线程:收集玩家发来的命令(简化问题:用一个数字代表),将命令数据写入一个队列
//一个线程:从队列中取出玩家发送来的命令,解析,执行玩家要干的动作
//使用list:容器(和vector内部的实现手法是不一样的)
//频繁的按照顺序插入和删除数据时list效率高
//对于随意插入和删除数据时vector效率高
A myobja;
//第二个参数时引用,才能保证线程里,用的时同一个对象
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
//操作的时候用代码把共享数据锁住,其他想操作共享数据的线程必须等待
//锁住后操作数据、解锁
//解锁后其他数据锁住,操作数据、解锁
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
2.std::lock_guard类模板
- std::lock_guard类模板:直接取代lock()和unlock();也就是说,使用了lock_guard()后就不能再使用lock()和unlock();
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
//如果没有锁,会不定时崩溃
my_mutex.lock();
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
bool outMsgLULProc(int &command)
{
//有lock_guard()后就不需要再用lock()和unlock()
//lock_guard() 构造函数执行了一次my_mutex.lock()
//析构函数中执行了一次my_mutex.unlock()
//但是解锁只能在析构(函数返回)时解锁,所以不是很灵活
//可以使用大括号,在大括号中写lock_guard(),这样出了作用域就会自动析构,进行解锁
std::lock_guard<std::mutex> sbguard(my_mutex);
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
//容器:专用于代表玩家发送过来的命令
list<int> msgRecvQueue;
std::mutex my_mutex; //创建一个互斥量
};
int main()
{
//数据共享:网络游戏服务器
//有两个自己创建的线程
//一个线程:收集玩家发来的命令(简化问题:用一个数字代表),将命令数据写入一个队列
//一个线程:从队列中取出玩家发送来的命令,解析,执行玩家要干的动作
//使用list:容器(和vector内部的实现手法是不一样的)
//频繁的按照顺序插入和删除数据时list效率高
//对于随意插入和删除数据时vector效率高
A myobja;
//第二个参数时引用,才能保证线程里,用的时同一个对象
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
//操作的时候用代码把共享数据锁住,其他想操作共享数据的线程必须等待
//锁住后操作数据、解锁
//解锁后其他数据锁住,操作数据、解锁
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
三、死锁
- 现实生活中的死锁:张三站在北京等李四,不动,李四站在深圳等张三,不动;
- c++中的死锁(至少有两把锁):比如有两把锁,锁一,锁二,两个线程,线程A,线程B;(业务:需要把两把锁都锁上)
- (1)线程A执行的时候,这个线程先锁锁一;然后去锁锁二;
- (2)出现了上下文切换,线程A被切换走了,线程B开始执行,这个线程先锁锁二成功;然后线程B去锁锁一;
- 此时此刻,死锁就发生了
- (3)线程A锁不了锁二,流程走不下去,所以锁一解不开;
- (4)线程B锁不了锁一,流程走不下去,所以锁二解不开;
1.死锁演示
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
//先锁一,后锁二
my_mutex1.lock();
//执行.......
my_mutex2.lock();
msgRecvQueue.push_back(i);
//unlock的顺序无所谓
my_mutex1.unlock();
my_mutex2.unlock();
}
}
bool outMsgLULProc(int& command)
{
//先锁二,后锁一
my_mutex2.lock();
//执行.......
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//unlock的顺序无所谓
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
//unlock的顺序无所谓
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
list<int> msgRecvQueue;
std::mutex my_mutex1; //创建第一个互斥量
std::mutex my_mutex2; //创建第二个互斥量
};
int main()
{
A myobja;
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
2.死锁的一般解决方案
- 死锁产生的原因:锁的顺序不一样,只要保证两个互斥量上锁的顺序一样,就不会产生死锁;
- 使用lock_guard()也是相同的;
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock_guard<std::mutex> sbguard1(my_mutex1);
std::lock_guard<std::mutex> sbguard2(my_mutex2);
msgRecvQueue.push_back(i);
//unlock的顺序无所谓
;
}
}
bool outMsgLULProc(int& command)
{
std::lock_guard<std::mutex> sbguard1(my_mutex1);
std::lock_guard<std::mutex> sbguard2(my_mutex2);
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
list<int> msgRecvQueue;
std::mutex my_mutex1; //创建第一个互斥量
std::mutex my_mutex2; //创建第二个互斥量
};
int main()
{
A myobja;
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
3.std::lock()函数模板
-用于处理多个互斥量;
一次锁住两个或者两个以上的互斥量(至少两个);(同时锁住多个互斥量的情况比较少见);
- 不存在在多线程中,因为锁的顺序导致死锁的风险问题;
- std::lock():如果互斥量中有一个每锁住,他就会释放自己锁住的,然后就等在那里,等所有互斥量都锁住,才往下走;
- 要么两个互斥量都锁住或者两个互斥量都释放;如果只锁一个,另外一个没成功,则立即释放锁住的;
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
//相当于每个互斥两都调用了lock();
//可以有多个参数
//必须对应解锁
std::lock(my_mutex1, my_mutex2);
msgRecvQueue.push_back(i);
//unlock的顺序无所谓
my_mutex1.unlock();
my_mutex2.unlock();
}
}
bool outMsgLULProc(int& command)
{
std::lock(my_mutex1, my_mutex2);
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
list<int> msgRecvQueue;
std::mutex my_mutex1; //创建第一个互斥量
std::mutex my_mutex2; //创建第二个互斥量
};
int main()
{
A myobja;
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
4.std::lock_guard的std::adopt_lock参数
- std::adopt_lock 可以使std::lock_guard()在构造函数不进行lock();
- std::adopt_lock是一个结构体对象,起一个标记作用,表示互斥量已经进行lock了,不需要在构造函数里面在对其进行lock;
#include <thread>
#include <mutex>
#include <iostream>
#include <list>
#include <windows.h>
using namespace std;
//成员函数作为线程函数的方法来写线程
class A
{
public:
//线程一:从玩家收到的消息入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock(my_mutex1, my_mutex2);
//std::adopt_lock 在构造函数不进行lock()
std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);
msgRecvQueue.push_back(i);
}
}
bool outMsgLULProc(int& command)
{
std::lock(my_mutex1, my_mutex2);
std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty())
{
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
//线程二:把数据从消息队列取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,插入一个元素" << i << endl;
}
else
{
cout << "outMsgRecvQueue()执行,队列为空!" << endl;
}
}
}
private:
list<int> msgRecvQueue;
std::mutex my_mutex1; //创建第一个互斥量
std::mutex my_mutex2; //创建第二个互斥量
};
int main()
{
A myobja;
thread myOutMsgobj(&A::outMsgRecvQueue, &myobja);
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutMsgobj.join();
myInMsgobj.join();
Sleep(10);
return 0;
}
- 建议一个一个的锁,尽量不要同时lock();同时lock()也不常见;