所需头文件
#include "stdafx.h"
#include <mutex>
#include <iostream>
#include <list>
using namespace std;
一.windows临界区
下面我们介绍一下,windows临界区,与我们前面介绍的C++中的mutex&非常类似,依然利用多线程学习(四)中的示例进行演示:包括了读和写线程
该示例说明如下:
假设做一个简易的网络游戏服务器:
两个自己创建的线程,一个线程收集玩家命令(用数字代表),并将命令写到一个队列中
另一个线程从队列中取出玩家发送来的命令,解析,然后执行玩家需要的动作
首先,需要额外包含头文件
#include <Windows.h>
#define __WINDOWSJQ_
线程入口函数
代码逻辑为如果打开了使用windows临界区的开关,则在读写线程中分别使用windows临界区进行共享数据保护,否则使用C++中的mutex,如下:
class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
#ifdef __WINDOWSJQ_ //如果打开了使用windows临界区的开关
EnterCriticalSection(&myWinSec); //进入临界区(加锁)
msgRecvQueue.push_back(i); //假设数字i为收到的命令
LeaveCriticalSection(&myWinSec); //退出临界区(解锁)
#else //否则使用mutex
myMutex.lock();
msgRecvQueue.push_back(i); //假设数字i为收到的命令
myMutex.unlock();
#endif // __WINDOWSJQ_
}
}
bool outMsgProc(int& command)
{
#ifdef __WINDOWSJQ_
EnterCriticalSection(&myWinSec);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
LeaveCriticalSection(&myWinSec);
return true;
}
LeaveCriticalSection(&myWinSec);
#else
myMutex.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
myMutex.unlock();
return true;
}
myMutex.unlock();
#endif // __WINDOWSJQ_
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result == true)
{
cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
}
}
cout << "end" << endl;
}
A()
{
#ifdef __WINDOWSJQ_
InitializeCriticalSection(&myWinSec); //用临界区之前要先初始化
#endif // __WINDOWSJQ_
}
private:
std::mutex myMutex; //代表玩家发送来的命令容器
std::list<int> msgRecvQueue; //互斥量
#ifdef __WINDOWSJQ_
CRITICAL_SECTION myWinSec; //windows中的临界区,类似于c++中的mutex
#endif
};
主函数
int main()
{
A myObj;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &myObj);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myObj);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
return 0;
}
运行上述程序,因为默认打开了使用windows临界区的开关,即语句#define _WINDOWSJQ,类似使用mutex一样,程序稳定运行。
二.多次进入临界区实验
在同一个线程中(不同线程就会卡住等待),windows中的“相同临界区变量”代表的临界区的进入(EnterCriticalSection)可以被多次调用
但是调用了几次EnterCriticalSection,就得调用几次LeaveCriticalSection
修改其中一个线程入口函数进行测试,如下:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
#ifdef __WINDOWSJQ_
EnterCriticalSection(&myWinSec); //两次进入临界区
EnterCriticalSection(&myWinSec);
msgRecvQueue.push_back(i); //假设数字i为收到的命令
LeaveCriticalSection(&myWinSec); //退出临界区
LeaveCriticalSection(&myWinSec);
#else
myMutex.lock();
msgRecvQueue.push_back(i); //假设数字i为收到的命令
myMutex.unlock();
#endif // __WINDOWSJQ_
}
}
那么C++允许多次加锁吗???答案是不允许的,会报异常(可以自行测试)
测试此种情况时,需要注释掉前面打开的#define __WINDOWSJQ_语句,并修改其中一个线程入口函数进行测试,如下:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
#ifdef __WINDOWSJQ_
EnterCriticalSection(&myWinSec);
msgRecvQueue.push_back(i); //假设数字i为收到的命令
LeaveCriticalSection(&myWinSec);
#else
myMutex.lock(); //多次加锁
myMutex.lock();
msgRecvQueue.push_back(i); //假设数字i为收到的命令
myMutex.unlock();
myMutex.unlock();
#endif // __WINDOWSJQ_
}
}
三.自动析构技术
前面多线程学习(四)已经介绍过std::lock_guard<std::mutex>,那么允许多次lock_guard吗???
我们根据多线程学习(四)知道,使用lock_guard就可以不用lock和unlock,替代了lock和unlock,可以避免忘记lock_guard,所以与lock和unlock类似,也不能多次lock_guard
与上面测试是否能多次lock类似,先注释掉前面打开的 #define __WINDOWSJQ_ 语句,修改其中一个线程入口函数如下:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
#ifdef __WINDOWSJQ_
EnterCriticalSection(&myWinSec);
msgRecvQueue.push_back(i); //假设数字i为收到的命令
LeaveCriticalSection(&myWinSec);
#else
std::lock_guard<std::mutex> myLockGuard1(myMutex);
std::lock_guard<std::mutex> myLockGuard2(myMutex);
msgRecvQueue.push_back(i); //假设数字i为收到的命令
#endif // __WINDOWSJQ_
}
}
修改后,运行程序发现程序报出异常,不能多次lock_guard
还有个问题,windows下有没有类似lock_guard的模板类,也就是避免忘记LeaveCriticalSection的方法呢?
不知道有没有,但我们可以自己实现,如下:
本类用于自动释放windows下的临界区,方式忘记LeaveCriticalSection,导致死锁情况发生,类似于C++中的std::lock_guard<std::mutex>
这种类叫做叫RAII类(Resource Acquisition is initialization),即“资源获取即初始化”
容器,智能指针这种类都属于RAII类
class CWinLock
{
public:
CWinLock(CRITICAL_SECTION* pWinSec) //构造函数
{
myWinSec = pWinSec;
EnterCriticalSection(myWinSec); //进入临界区
}
~CWinLock() //析构函数
{
LeaveCriticalSection(myWinSec); //退出临界区
}
private:
CRITICAL_SECTION* myWinSec;
};
下面在程序中测试是否可以正常使用,如下:
测试其中一个线程入口函数即可,记得取消注释#define __WINDOWSJQ_语句
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
#ifdef __WINDOWSJQ_
//EnterCriticalSection(&myWinSec);
//EnterCriticalSection(&myWinSec);
CWinLock CWL1(&myWinSec); //创建CWinLock对象,也RAII对象
CWinLock CWL2(&myWinSec); //调用多次也可以
msgRecvQueue.push_back(i); //假设数字i为收到的命令
//LeaveCriticalSection(&myWinSec);
//LeaveCriticalSection(&myWinSec);
#else
std::lock_guard<std::mutex> myLockGuard1(myMutex);
//myMutex.lock();
msgRecvQueue.push_back(i); //假设数字i为收到的命令
//myMutex.unlock();
#endif // __WINDOWSJQ_
}
}
修改后运行程序,程序运行正常,且可以多次调用,这样就避免了忘记退出临界区的操作LeaveCriticalSection。
四.recursive_mutex递归的独占互斥量
std::mutex:独占互斥量,自己lock时,别人lock不了
recursive_mutex:递归的独占互斥量,允许同一个线程,同一个互斥量多次lock
与mutex一样,也有lock和unlock
首先有个问题,在什么情况下,我们需要多次lock呢?
下面演示一下这种情况:
在类A中创建两个测试函数testFunc,这两个函数中调用了lock_guard,并在其中一个线程入口函数inMsgRecvQueue中调用testFunc1,则一共调用了三次lock,程序运行崩溃。
class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
std::lock_guard<std::mutex> myLockGuard1(myMutex);
testFunc1(); //调用此函数后,一共lock三次,程序崩溃
//myMutex.lock();
msgRecvQueue.push_back(i); //假设数字i为收到的命令
//myMutex.unlock();
}
}
bool outMsgProc(int& command)
{
myMutex.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
myMutex.unlock();
return true;
}
myMutex.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result == true)
{
cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
}
}
cout << "end" << endl;
}
void testFunc1()
{
std::lock_guard<std::mutex> myLockGuard1(myMutex);
//...执行其他程序
testFunc2(); //这个函数里还会加锁,崩溃
}
void testFunc2()
{
std::lock_guard<std::mutex> myLockGuard2(myMutex);
//...执行其他程序
}
private:
std::mutex myMutex; //代表玩家发送来的命令容器
std::list<int> msgRecvQueue; //互斥量
};
recursive_mutex与mutex用法类似,但可以解决多次lock问题
下面进行recursive_mutex的用法演示:修改类A中lock_guard模板类型为recursive_mutex
class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
std::lock_guard<std::recursive_mutex> myLockGuard1(myMutex);
testFunc1(); //调用此函数后,一共lock三次,
msgRecvQueue.push_back(i); //假设数字i为收到的命令
}
}
bool outMsgProc(int& command)
{
myMutex.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
myMutex.unlock();
return true;
}
myMutex.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result == true)
{
cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
}
}
cout << "end" << endl;
}
void testFunc1()
{
std::lock_guard<std::recursive_mutex> myLockGuard1(recursive_mutex);
//...执行其他程序
testFunc2(); //这个函数里还会加锁,崩溃
}
void testFunc2()
{
std::lock_guard<std::recursive_mutex> myLockGuard2(recursive_mutex);
//...执行其他程序
}
private:
std::recursive_mutex myMutex; //互斥量
std::list<int> msgRecvQueue; //代表玩家发送来的命令容器
};
修改后运行程序,程序运行正常,则recursive_mutex可以解决多次lock的问题。
虽然recursive_mutex解决了多次lock的问题,但是代码是否有优化空间呢?
recursive_mutex的效率比mutex效率差一些,据说递归次数有限制,递归太多次可能报异常,可自行测试。
五.带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex:带超时功能的独占互斥量
std::recursive_timed_mutex:带超时功能的递归独占互斥量
try_lock_for():参数是一段时间,等待一段时间,如果拿到锁(lock成功),执行正常拿到锁的程序,如果等待超过时间没拿到锁,就继续处理其他程序
try_lock_until():参数是一个未来的时间点,在未来的时间点没到的时间内,如果拿到锁,就正常执行拿到锁后的程序,没拿到锁处理其他程序
下面先测试try_lock_for函数的使用:同样修改类A中的代码如下:
class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
std::chrono::milliseconds timeout(100);//100ms
if (myMutex.try_lock_for(timeout)) //等待100ms尝试获取锁
{
//在这100ms之内拿到锁
msgRecvQueue.push_back(i); //假设数字i为收到的命令
myMutex.unlock(); //用完要解锁
}
else //让另一个线程lock后先停止一会,也就是先不unlock,可测试此种情况
{
//没拿到锁,程序停一会
std::chrono::milliseconds sleeptime(100);//100ms
std::this_thread::sleep_for(sleeptime);
cout << "lock失败" << endl;
}
}
}
bool outMsgProc(int& command)
{
myMutex.lock();
std::chrono::milliseconds sleeptime(1000);//1s 测试拿不到锁的情况
std::this_thread::sleep_for(sleeptime);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
myMutex.unlock();
return true;
}
myMutex.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result == true)
{
cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::timed_mutex myMutex; //带超时功能的独占互斥量
std::list<int> msgRecvQueue; //代表玩家发送来的命令容器
};
修改后运行程序,可以看到有获取不到锁的情况,但程序没有卡住,依然继续处理其他程序。
下面演示try_lock_until的用法:与try_lock_for类似,只是参数不一样,可实现同样的功能
class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvqueue执行,插入一个元素" << i << endl;
std::chrono::milliseconds tiemout(100); //100ms
if (myMutex.try_lock_until(chrono::steady_clock::now()+tiemout)) //最多等待100ms,参数为当前时间+100ms
{
//在这100ms之内拿到锁
msgRecvQueue.push_back(i); //假设数字i为收到的命令
myMutex.unlock(); //用完要解锁
}
else //让另一个线程lock后先停止一会,也就是先不unlock,可测试此种情况
{
//没拿到锁,程序停一会
std::chrono::milliseconds sleeptime(100);//100ms
std::this_thread::sleep_for(sleeptime);
cout << "lock失败" << endl;
}
}
}
bool outMsgProc(int& command)
{
myMutex.lock();
std::chrono::milliseconds sleeptime(1000);//1s 测试拿不到锁的情况
std::this_thread::sleep_for(sleeptime);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); //返回第一个元素但不检查元素是否存在
msgRecvQueue.pop_front();
myMutex.unlock();
return true;
}
myMutex.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgProc(command);
if (result == true)
{
cout << "outMsgRecvQueue执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue执行,但目前还是空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::timed_mutex myMutex; //带超时功能的独占互斥量
std::list<int> msgRecvQueue; //代表玩家发送来的命令容器
};
std::recursive_timed_mutex的用法这里就不演示了,比std::timed_mutex多了个多次lock的功能,相信大家也已经知道怎么使用了。
网友评论