05C++11多线程编程之使用正确的共享代码案例引入互斥量mutex的概念、用法
1 共享代码案例概念分析
-
1)网络游戏服务器。两个自己创建的线程,一个线程收集玩家命令(一个数字代表玩家发来的命令),并把命令写到一个队列中;另外一个线程从队列中取出玩家送来的命令,解析,然后执行玩家需要的动作。
-
2)但是,很多时候我们可以利用该思想进行衍生。例如有一个回调函数分为上下线的玩家ID,我们需要处理这些触发回调的上下线玩家id。那么我们就可以将这些上下线作为switch的case 值,玩家id作为处理后续操作的变量先将他们保存在一个结构体,再push进队列。这样就可以新开一个线程进行处理。
这个例子下:回调函数充当了push消息线程。并且将处理玩家id上下线的逻辑放到新线程中处理,这样做的好处是简化了直接在回调函数中处理,防止回调处理过长出现崩溃。
注:回调函数的形参一般带有void *pUser,以便传入该处理消息类的对象。
上面这种思想在开发中非常常见,很重要。
下面为了简单,所以使用第一种处理思想处理共享代码案例。这种类一般称之为消息分发类或者消息处理类。
使用list的原因:
list:频繁的按顺序插入和删除数据时效率高。vector容器随机的插入删除数据效率高!
在讲安全正确的共享代码案例之前,我们必须先引入互斥量的概念。
2 互斥量的概念
- 1)互斥量作用:保护一段共享代码。
- 2)互斥量是个类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁成功(成功的标志是lock()函数返回);如果没锁成功,那么这个流程卡在lock(),其它线程不断的尝试去锁这把锁头。
互斥量使用要小心,保护数据不多也不能少,少了,没有达到保护的效果,多了,影响效率。
3 互斥量的用法
- 1)步骤:先lock(),操作共享数据,unlock()。
- 2)lock()与unlock()必须要成对使用,有lock()必然要有unlock(),每调用一次lock(),必然应该调用一次unlock()。不应该也不允许调用一次lock(),却调用了2次unlock(),这些非对称数量的调用都会导致代码不稳定或者阻塞甚至崩溃。并且很难排除。
代码:
#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
using namespace std;
//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A{
public:
//把收到的消息入到一个队列的线程
void inMsgRecvQueue(){
for (int i = 0; i < 10000; i++){
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
my_mutex.lock();
//my_mutex.lock();//error,多次上锁程序崩溃
msgRecvQueue.push_back(i); //存放消息
my_mutex.unlock();
//my_mutex.unlock();//error,多次解锁程序崩溃
}
}
//将共享的代码放到一个函数处理,方便上锁解锁
bool outMsgLULProc(int &command){
my_mutex.lock();
if (!msgRecvQueue.empty()){
//my_mutex.lock();//必须在读msgRecvQueue.empty()之前上锁,防止多个线程都满足不为空,然后阻塞完后就进行消费,这是不行的
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex.unlock(); //所有分支都必须有unlock()
return true;
}
my_mutex.unlock();
return false;
}
//把数据从消息队列取出的线程
void outMsgRecvQueue(){
int command = 0;
for (int i = 0; i < 10000; i++){
bool result = outMsgLULProc(command);
if (result == true){
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//处理数据
//这里有可能也有需要共享的代码段
}
else{
//消息队列为空
cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
}
}
cout << "end!" << endl;
}
private:
std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
std::mutex my_mutex;
};
int main()
{
A myobja;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,地址,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
cout << "主线程执行!" << endl;
return 0;
}
正确结果:
两次上锁,一次解锁的错误结果(注Linux下的C接口两次上锁结果是会阻塞):
一次上锁,两次解锁的错误结果:
所以总结:C++凡是mutex的lock和unlock不成对调用,都会直接报上面的程序崩溃错误。其它的C语言可能会阻塞或者其它错误。