创建多个线程、数据共享问题分析、案例代码
创建和等待多个线程
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
using namespace std;
//线程入口函数
void myprint(int inum)
{
cout << "myprint线程开始执行,线程编号 = " << inum << endl;
//...........
cout << "myprint线程执行结束,线程编号 = " << inum << endl;
return;
}
int main()
{
vector<thread> mythreads;
//创建十个线程,线程入口函数统一使用myprint
for (int i = 0; i < 10; i++)
{
mythreads.push_back(thread(myprint, i));//创建并开始执行线程
}
for (auto iter = mythreads.begin();iter != mythreads.end();iter++)
{
iter->join(); //等待十个线程都返回
}
cout << "主线程执行完毕" << endl;
return 0;
}
运行结果:
myprint线程开始执行,线程编号 = myprint线程开始执行,线程编号 = myprint线程开始执行,线程编号 = 4
myprint线程执行结束,线程编号 = 4
myprint线程开始执行,线程编号 = 5
myprint线程执行结束,线程编号 = 5
myprint线程开始执行,线程编号 = 9
myprint线程执行结束,线程编号 = 9
myprint线程开始执行,线程编号 = 8
myprint线程执行结束,线程编号 = 8
1
myprint线程执行结束,线程编号 = 1
myprint线程开始执行,线程编号 = 7
myprint线程执行结束,线程编号 = 7
myprint线程开始执行,线程编号 = 0
myprint线程执行结束,线程编号 = 0
2
myprint线程执行结束,线程编号 = 2
myprint线程开始执行,线程编号 = 3
myprint线程执行结束,线程编号 = 3
myprint线程开始执行,线程编号 = 6
myprint线程执行结束,线程编号 = 6
主线程执行完毕
可以看见,线程执行的顺序是混乱的,这与计算机内部的线程运行调度机制有关.
将线程放到容器中管理,对大量线程的管理很方便。
数据共享问题分析
只读数据
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
using namespace std;
vector<int> g_v = { 1,2,3 }; //共享数据 只读
//线程入口函数
void myprint(int inum)
{
//cout << "myprint线程开始执行,线程编号 = " << inum << endl;
...........
//cout << "myprint线程执行结束,线程编号 = " << inum << endl;
cout << "id 为 " << std::this_thread::get_id() << "的线程 打印g_v值" << g_v[0] << g_v[1] << g_v[2] << endl;
return;
}
int main()
{
vector<thread> mythreads;
//创建十个线程,线程入口函数统一使用myprint
for (int i = 0; i < 10; i++)
{
mythreads.push_back(thread(myprint, i));//创建并开始执行线程
}
for (auto iter = mythreads.begin();iter != mythreads.end();iter++)
{
iter->join(); //等待十个线程都返回
}
cout << "主线程执行完毕" << endl;
return 0;
}
执行结果:
id 为 20924的线程 打印g_v值12id 为 28192的线程 打印g_v值123
id 为 14616的线程 打印g_v值123
3
id 为 26844的线程 打印g_v值123
id 为 27624的线程 打印g_v值123
id 为 25476的线程 打印g_v值123
id 为 24024的线程 打印g_v值123
id 为 26920的线程 打印g_v值123
id 为 17628的线程 打印g_v值123
id 为 25048的线程 打印g_v值123
主线程执行完毕
可以看见,只读的数据是安全稳定的,不管怎么读,读出来的数据都是固定的,对于这种数据直接读取就可以了。
可读可写的数据
两个线程写数据,八个线程读数据,如果代码没有特别处理,程序肯定崩溃。
最简单的处理:读的时候不能写,写的时候不能读,两个线程不能同时写,八个线程不能同时读。(互斥锁?排他锁?)
共享数据的保护案例代码
开发一个简单的网络游戏服务器,有两个自己创建的线程,一个线程收集玩家命令,并把命令数据写到一个队列中。
另外一个线程,从队列中取出玩家发出来的命令,解析,然后执行玩家需要的动作。
假定每次发出的命令为一个数字。用list容器。list:频繁的按顺序插入和删除数据时效率高。vector对于随机插入和删除数据效率高。
互斥量概念、用法、死锁演示及解决
互斥量(mutex)的基本概念
互斥量是一个类对象。可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能够锁定成功(成功的标志是lock()返回,如果没有锁成功,那就会一直卡住等待枷锁成功)。
互斥量使用要小心,保护数据少了达不到保护效果,多了会影响效率。
互斥量的用法
lock(), unlock()
步骤:先lock()操作共享数据。然后再unlock()共享数据。两个函数必须匹配。
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
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);//假设数字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;
//cout << "outMsgRecvQueue()执行,但是目前消息队列为空 " << i << endl;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(i);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
}
else
{
cout << "消息序列为空" << endl;
}
}
cout << "end" << endl;
return;
}
private:
list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
std::mutex my_mutex;
};
int main()
{
A myobja;
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
myOutnMsgobj.join();
myInMsgobj.join();
return 0;
}
加入锁之后程序不会出现异常。
std::lock_guard类模板
为了防止忘记unlock(),引入了std::lock_guard类模板,可以自动unlock();
有点类似于只能指针(unique_ptr<>)。直接取代lock()和unlock();也就是说,用过std::lock_guard类模板之后,不能使用lock()和unlock();
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
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);//假设数字i就是收到的命令,直接加入消息队列
my_mutex.unlock();
}
}
bool outMsgLULProc(int& command)
{
std::lock_guard<std::mutex> sbguard(my_mutex);
//my_mutex.lock();
if (!msgRecvQueue.empty())
{
//不为空
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//my_mutex.unlock();
return true;
//这里处理数据........
//..............
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(i);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
}
else
{
cout << "消息序列为空" << endl;
}
}
cout << "end" << endl;
return;
}
private:
list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
std::mutex my_mutex;
};
int main()
{
A myobja;
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
myOutnMsgobj.join();
myInMsgobj.join();
return 0;
}
lock_guard构造函数里面执行了mutex::lock();
lock_guard析构函数里执行了mutex::unlock();
根据原理想要在任何地方unlock(),就可以在程序中想析构的地方加入花括号{};他的作用域就在花括号中。
死锁
比如有两把锁(死锁产生的前提条件是 由至少两个互斥量也就是两把锁才能产生),金锁(jinlock),银锁(yinlock);
假设线程A,B;
(1)线程A执行的时候将金锁锁住,lock()成功,然后它去lock银锁时;
(2)线程B执行正好已经锁住银锁,然后它去lock金锁。
这时,线程A一直尝试锁住银锁,金锁一直没有解锁,线程B一直尝试金锁,银锁一直没有解锁,两边线程一直等待,就造成了死锁。
死锁演示
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;
class A
{
public:
//把收到的消息放入队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
my_mutex1.lock(); //实际工程中这两个lock时机可能不一定挨着,可能他们需要保护不同的数据共享块
my_mutex2.lock();
msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
my_mutex2.unlock();
my_mutex1.unlock();
}
}
bool outMsgLULProc(int& command)
{
//std::lock_guard<std::mutex> sbguard(my_mutex1);
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
//不为空
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
//my_mutex.unlock();
return true;
//这里处理数据........
//..............
}
//消息队列为空
my_mutex1.unlock();
my_mutex2.unlock();
return false;
//cout << "outMsgRecvQueue()执行,但是目前消息队列为空 " << i << endl;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(i);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
}
else
{
cout << "消息序列为空" << endl;
}
}
cout << "end" << endl;
return;
}
private:
list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
std::mutex my_mutex1;//一个互斥量
std::mutex my_mutex2;//另一个互斥量
};
int main()
{
A myobja;
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
myOutnMsgobj.join();
myInMsgobj.join();
return 0;
}
因为一边是先锁1再锁2,一边是先锁2再锁1,就发生了死锁。
死锁的一般解决方案
只要保证两个互斥量上锁的顺序一致,就不会死锁。
std::lock()函数模板
一次锁住两个或者两个以上的互斥量(至少两个);
它不存在因为在多个线程中因为锁的顺序问题导致死锁的风险问题。
std::lock():如果互斥量中有一个没有锁住,就等待所有互斥量锁住才往下走(返回)。
std::mutex my_mutex1;//一个互斥量
std::mutex my_mutex2;//另一个互斥量
用这个函数的话会同时锁住两个互斥量。不会出现只锁住一个的情况。如果只锁定一个另一个没有成功,就立即解锁已经锁住的锁。
std::lock(my_mutex1, my_mutex2);
代替两个锁的lock()即可,但是后面还是要记得unlock();
std::lock_guard的std::adopt_lock参数
在std::lock_guard构造的时候加入std::adopt_lock 就相当于这个互斥量已经lock()过了。在使用std::lock_guard中不加锁,在释放对象的时候直接解锁!
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
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::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);//假设数字i就是收到的命令,直接加入消息队列
}
}
bool outMsgLULProc(int& command)
{
//std::lock_guard<std::mutex> sbguard(my_mutex1);
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(i);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
}
else
{
cout << "消息序列为空" << endl;
}
}
cout << "end" << endl;
return;
}
private:
list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
std::mutex my_mutex1;//一个互斥量
std::mutex my_mutex2;//另一个互斥量
};
int main()
{
A myobja;
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
myOutnMsgobj.join();
myInMsgobj.join();
return 0;
}
unique_lock详解
unique_lock是一个类模板,工作中,一般lock_guard(推荐使用);
lock_guard取代了mutex的lock()和unlock();unique_lock比起lock_guard灵活了很多,但是效率要低一些。
unique_lock取代lock_guard
直接替换没有任何区别。
unique_lock的第二个参数
lock_guard可以带第二个参数: std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
,第二个参数起标记作用。
unique_lock第二个参数一样起标记作用。
std::adopt_lock
表示这个互斥量已经被lock(必须要把互斥量提前lock)。
std::adopt_lock标记的效果是“假设调用方线程已经拥有了互斥的所有权”,通知unique_lock不在 构造函数中lock这个互斥量。
std::try_to_lock
我们尝试用mutex的lock()去锁定这个mutex,如果没有锁定成功,也会立即返回,不会阻塞到那里。
使用try_to_lock不能自己先lock,相当于锁了两次。
#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;
class A
{
public:
//把收到的消息放入队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
std::unique_lock<std::mutex> sbguard1(my_mutex1, std::try_to_lock);//尝试加锁
if (sbguard1.owns_lock())//如果拿到了锁
{
msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行但未能拿到锁。" << endl;
}
}
}
bool outMsgLULProc(int& command)
{
std::unique_lock<std::mutex> sbguard1(my_mutex1);
std::chrono::milliseconds dura(20000);
std::this_thread::sleep_for(dura);//休息20s
if (!msgRecvQueue.empty())
{
//不为空
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(i);
if (result)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
}
else
{
cout << "消息序列为空" << endl;
}
}
cout << "end" << endl;
return;
}
private:
list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
std::mutex my_mutex1;//一个互斥量
std::mutex my_mutex2;//另一个互斥量
};
int main()
{
A myobja;
thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
myOutnMsgobj.join();
myInMsgobj.join();
return 0;
}
运行结果:
inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1924
inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1925
inMsgRecvQueue()执行但未能拿到锁。
inMsgRecvQueue()执行,插入一个元素 1926
inMsgRecvQueue()执行但未能拿到锁。
20s之后就会拿到锁开始执行。
std::defer_lock
用defer_lock的前提是,你自己不能先lock,否则会报异常。
defer_lock的意思是没有给mutex加锁:初始化了一个没有加锁的mutex。
std::unique_lock<std::mutex> sbguard1(my_mutex1,std::defer_lock);//没加锁的mutex1
sbguard1.lock();//不用自己unlock
unique_lock的成员函数
lock()
使用unique_lock的时候,实例化的对象lock()后不用unlock();但是可以手动unlock。
unlock()
可以提前解锁,之后还是可以加锁。
try_lock()
尝试给互斥量加锁,如果拿不到锁,就返回false,如果拿到了,就返回true。
if (sbguard1.try_lock() == true)
{
msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
}
else
{
//没拿到锁
cout << "inMsgRecvQueue()执行但未能拿到锁。" << endl;
}
}
release()
返回它所管理的mutex指针,并释放所有权,也就是说,这个unique_lock和mutex没有任何联系。
std::unique_lock<std::mutex> sbguard1(my_mutex1);
std::mutex* ptx = sbguard1.release();//释放这个对象关联的互斥量对象后,用ptx指针接管。然后需要手动解锁。
ptx->unlock();
unique_lock所有权的传递
所有权不能复制,需要用std::move函数:
std::unique_lock<std::mutex> sbguard1(my_mutex1);
std::unique_lock<std::mutex> sbguard2(std::move(sbguard1));
还可以return unique_lock类的对象。