C++11并发与多线程笔记(3)创建多个线程、数据共享问题分析、案例代码
创建和等待多个线程
void myPrint(int num) {
cout << "myPrint线程开始执行了,线程编号=" << num << endl;
cout << "myPrint线程结束执行了,线程编号=" << num << endl;
}
vector<thread> mythreads;
//创建10个线程,线程入口函数统一使用myPrint.
//1、多个线程执行顺序是乱的,跟操作系统内部对线程运行的调度有关
//2、主线程等待所有子线程运行结束,最后主线程结束,推荐join写法,更容易写出稳定的程序。
//3、把thread对象放入到容器里面管理,看起来像个thread对象数组,这对我们一次创建大量的线程并对大量线程进行管理来说非常方便。
for (int i = 0; i < 10; i++) {
mythreads.push_back(thread(myPrint, i));//创建10个线程已经开始执行
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); iter++) {
iter->join();
}
数据共享问题分析
只读的数据
vector<int> g_v = { 1,2,3 };//共享数据,只读的
void myPrint(int num) {
cout << "id为:" << std::this_thread::get_id() << "的线程 打印g_v值" << g_v[0] << g_v[1] << g_v[2] << endl;
}
vector<thread> mythreads;
for (int i = 0; i < 10; i++) {
mythreads.push_back(thread(myPrint, i));//创建10个线程已经开始执行
}
for (auto iter = mythreads.begin(); iter != mythreads.end(); iter++) {
iter->join();
}
有读有写
2.2有读有写,两个线程写,八个线程读,如果代码没有特别的处理,那程序肯定崩溃;
最简单的不崩溃处理,读的时候不能写,写的时候不能读,2个线程不能同时写,8个线程不能同时读;
假设写的动作分10小步,由于任务切换导致各种诡异事情发生,最可能的诡异事情还是崩溃。
其它案例
2.3其它案例
数据共享:
北京->深圳 火车T23,10个窗口售票,1,2号窗口都要订99座
共享数据的保护案例代码
网络游戏服务器,两个自己创建的线程,一个线程手机玩家命令(用一串数字代表玩家发来的命令),并把命令数据写到一个队列中。
另外一个线程从队列中取出外加发来的命令,解析,然后执行玩家需要的动作。
vector,list,list跟vector。list:频繁的按顺序插入和删除数据时效率高。vector容器随机插入和删除数据效率高。
准备用成员函数作为线程函数的方法来写线程,
代码化解决问题:引入一个c++解决多线程保护共享数据问题的第一个概念“互斥量”。
互斥量概念、用法、死锁演示及解决详解
互斥量(mutex)的基本概念
互斥量是个类对象,理解为一把锁,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标志是lock()函数返回了),如果没锁成功,那么流程卡在lock()这里不断尝试去锁这把锁头,互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果,多了,影响效率。
互斥量的用法
lock(),unlock()
class A
{
public:
//把收到消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 10000; 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();//程序有两个出口,需要两个unlock();
return true;
}
my_mutex.unlock();//必须使用同一个互斥量加锁解锁
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 100000; i++) {
int command = 0;
bool result = outMsgLULProc(command);
if (result) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else {
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl; }
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令。
std::mutex my_mutex;//创建了一个互斥量(一个互斥量就是一把锁)
};
//main:
A a;
thread outThread(&A::outMsgRecvQueue,&a);//第二个参数是引用,才能保证线程里面使用的是同一个对象
thread inThread(&A::inMsgRecvQueue, &a);
//同时读写,迟早会出问题,比如一个线程正在写,还没写完,另外一个线程就对写进去的数据进行读并且删除的操作。会发生冲突
outThread.join();
inThread.join();
//保护共享数据,操作时,某个线程用代码把共享数据锁住、操作数据,解锁,
// 其它想操作共享数据的线程必须等待解锁
步骤:先lock(),操作共享数据,unlock();
lock()和unlock()要成对使用,有lock()必然要有unlock(),每调用一次lock(),必然应该调用一次unlock(),
不应该也不允许调用1次lock()却调用了2次unlock(),也不允许调用2次lock()却调用1次unlock(),这些非对称数量的调用都会带来问题
有lock(),忘记unlock问题,非常难以排查
为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板,你忘记unlock()不要紧,我替你unlock();
学习过智能指针(unique_ptr<>):你忘记释放内存不要紧,我给你释放,
std:: lock_guard类模板
class A
{
public:
//把收到消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue执行 插入一个元素:" << i << endl;
std::lock_guard<std::mutex> guard(my_mutex);
msgRecvQueue.push_back(i);
}
}
bool outMsgLULProc(int& command) {
std::lock_guard<std::mutex> guard(my_mutex);//lock_guard构造函数里执行了mutex::lock()
//lock_guard析构函数执行了mutex::unlock();
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 100000; i++) {
int command = 0;
bool result = outMsgLULProc(command);
if (result) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else {
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令。
std::mutex my_mutex;//创建了一个互斥量(一个互斥量就是一把锁)
};
//main:
A a;
thread outThread(&A::outMsgRecvQueue,&a);//第二个参数是引用,才能保证线程里面使用的是同一个对象
thread inThread(&A::inMsgRecvQueue, &a);
//同时读写,迟早会出问题,比如一个线程正在写,还没写完,另外一个线程就对写进去的数据进行读并且删除的操作。会发生冲突
outThread.join();
inThread.join();
//保护共享数据,操作时,某个线程用代码把共享数据锁住、操作数据,解锁,
// 其它想操作共享数据的线程必须等待解锁
std::lock_guard类模板:直接取代lock()和unlock():也就是说,你用了lock_guard之后,再也不能使用lock()和unlock();
死锁
张三:站在北京 等李四,不挪窝;
李四:站在深圳 等张三,不挪窝;
比如我有两把锁(死锁这个问题,是由至少两个锁头也就是两个互斥量才能产生);金锁(JinLock),银锁(YinLock())
两个线程A,B
(1)线程A执行的时候,这个线程先锁金锁,把金锁lock()成功了,然后它去lock()银锁
出现了上下文切换
(2)线程B执行了,这个线程先锁 银锁,因为银锁还没有被锁,所以银锁会lock()成功,线程B要去lock金锁;
此时此刻,死锁就产生了。
(3)线程A因为拿不到银锁头,流程走不下去(所有后面代码有解金锁头的但是流程走不下去,,所以金锁头解不开);
(4)线程B因为拿不到金锁头,流程走不下去(所有后面代码有解银锁头的但是流程走不下去,,所以银锁头解不开);
大家都晾在这里,你等我,我等你
死锁演示
class A
{
public:
//把收到消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue执行 插入一个元素:" << i << endl;
my_mutex1.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
bool outMsgLULProc(int& command) {
my_mutex2.lock();
my_mutex1.lock();
std::lock(my_mutex1, my_mutex2);
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex2.unlock();
my_mutex1.unlock();
return true;
}
my_mutex2.unlock();
my_mutex1.unlock();
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 100000; i++) {
int command = 0;
bool result = outMsgLULProc(command);
if (result) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else {
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令。
std::mutex my_mutex1;//创建了一个互斥量(一个互斥量就是一把锁)
std::mutex my_mutex2;//创建了一个互斥量(一个互斥量就是一把锁)
};
//main:
A a;
thread outThread(&A::outMsgRecvQueue,&a);//第二个参数是引用,才能保证线程里面使用的是同一个对象
thread inThread(&A::inMsgRecvQueue, &a);
outThread.join();
inThread.join();
死锁的一般解决方案
只要保证这两个互斥量上锁的顺序保持一致就不会死锁。
std::lock()函数模板
std::lock()函数模板:多个互斥量才出场
能力:一次锁住两个或者两个以上的互斥量(至少两个,多了不限,一个不行);
它不存在这种因为在多线程中 ,因为锁的顺序问题导致死锁的风险问题。
std::lock():如果互斥量中有一个没锁柱,他就在那里等着,等所有互斥量都锁住,他才能往下走;
要么两个互斥量都锁柱,要么两个互斥量都没锁柱。(如果只锁了一个,另外一个没锁柱,则它立刻把已经锁住的解锁)
class A
{
public:
//把收到消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue执行 插入一个元素:" << i << endl;
std::lock(my_mutex1, my_mutex2);
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock();
}
}
bool outMsgLULProc(int& command) {
std::lock(my_mutex1, my_mutex2);
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_mutex2.unlock();
my_mutex1.unlock();
return true;
}
my_mutex2.unlock();
my_mutex1.unlock();
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 100000; i++) {
int command = 0;
bool result = outMsgLULProc(command);
if (result) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else {
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令。
std::mutex my_mutex1;//创建了一个互斥量(一个互斥量就是一把锁)
std::mutex my_mutex2;//创建了一个互斥量(一个互斥量就是一把锁)
};
//main:
A a;
thread outThread(&A::outMsgRecvQueue,&a);//第二个参数是引用,才能保证线程里面使用的是同一个对象
thread inThread(&A::inMsgRecvQueue, &a);
outThread.join();
inThread.join();
std::lock_guard的std::adopt_lock参数。
3.4std::lock_guard的std::adopt_lock参数
std::adopt_lock是个结构体对象,表明这个互斥量已经lock,不需要在lock_guard构造函数里面再对mutex对象进行lock了;
总结:std::lock()一次锁定多个互斥量,谨慎使用(建议一个一个用);
class A
{
public:
//把收到消息(玩家命令)入到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "inMsgRecvQueue执行 插入一个元素:" << i << endl;
std::lock(my_mutex1, my_mutex2);
std::lock_guard<mutex> guard1(my_mutex1, std::adopt_lock);
std::lock_guard<mutex> guard1(my_mutex1, std::adopt_lock);
msgRecvQueue.push_back(i);
}
}
bool outMsgLULProc(int& command) {
std::lock(my_mutex1, my_mutex2);
std::lock_guard<mutex> guard1(my_mutex1, std::adopt_lock);
std::lock_guard<mutex> guard1(my_mutex1, std::adopt_lock);
if (!msgRecvQueue.empty()) {
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue() {
for (int i = 0; i < 100000; i++) {
int command = 0;
bool result = outMsgLULProc(command);
if (result) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else {
cout << "outMsgRecvQueue()执行,但目前消息队列中为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令。
std::mutex my_mutex1;//创建了一个互斥量(一个互斥量就是一把锁)
std::mutex my_mutex2;//创建了一个互斥量(一个互斥量就是一把锁)
};
//main:
A a;
thread outThread(&A::outMsgRecvQueue,&a);//第二个参数是引用,才能保证线程里面使用的是同一个对象
thread inThread(&A::inMsgRecvQueue, &a);
outThread.join();
inThread.join();