第四节 创建多个线程、数据共享问题分析、案例代码
一、创建和等待多个线程
1.1 多个线程的执行顺序
#include<iostream>
#include<vector>
#include<thread>
#include<string>
using namespace std;
//线程入口函数
void myprint(int num){
cout<<"myprint线程开始执行了,线程编号= "<<num<<endl;
//
cout<<"myprint线程结束了,线程编号= "<<num<<endl;
return;
}
int main(){
//创建和等待多个线程
vector<thread> mythreads;
//创建10个线程,每个线程统一使用myprint作为入口函数
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();
}
cout<<"i love china"<<endl;
return 0;
}
输出:
myprint线程开始执行了,线程编号= 0
myprint线程结束了,线程编号= 0
myprint线程开始执行了,线程编号= 1
myprint线程结束了,线程编号= 1
myprint线程开始执行了,线程编号= 2
myprint线程结束了,线程编号= 2
myprint线程开始执行了,线程编号= 3
myprint线程结束了,线程编号= 3
myprint线程开始执行了,线程编号= 4
myprint线程结束了,线程编号= 4
myprint线程开始执行了,线程编号= 5
myprint线程结束了,线程编号= 5
myprint线程开始执行了,线程编号= 6
myprint线程结束了,线程编号= 6
myprint线程开始执行了,线程编号= 7
myprint线程结束了,线程编号= 7
myprint线程开始执行了,线程编号= 8
myprint线程结束了,线程编号= 8
myprint线程开始执行了,线程编号= 9
myprint线程结束了,线程编号= 9
i love china
多个线程的执行顺序可能是乱的(说实话,我运行了多次,我的没乱,可能是编译器和操作系统不同导致的),乱,和操作系统内部对线程的调用机制相关。
1.2 主线程等待所有的子线程运行结束,最后主线程结束,使用的是join,使用join更容易写出稳定的程序
1.3 把thread对象放入容器进行管理,便于对一次性创建的大量线程进行管理
二、数据共享问题
2.1 只读的数据
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;
return;
}
int main(){
//创建和等待多个线程
vector<thread> mythreads;
//创建10个线程,每个线程统一使用myprint作为入口函数
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();
}
cout<<"i love china"<<endl;
return 0;
}
设置g_v为只读数据,此时程序是安全稳定的。
2.2 有读有写
两个线程写,8个线程读。如果不进行线程对共享数据的同步互斥控制,极有可能会在线程切换的过程中导致各种问题。
案例:
北京—>深圳 火车 T123, 10个售票窗口买票, 1和2窗口同时都要订99号座位。如果1窗口和2窗口同时订,就会产生错误。
即只有实现线程同步,才能保证程序的正确运行。这就涉及达到《操作系统》中的进程和线程管理,同步互斥的概念可以在《操作系统》中进行学习。
三.如何保证安全访问共享数据
3.1 共享数据的保护案例
网络游戏服务器。两个自己创建的线程,一个线程收集玩家命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列当中;而另一个线程从队列当中取出玩家发送来的命令,解析,然后执行玩家需要的动作。
这里的队列可以用vector或者list来实现,二者各有优点;
list:频繁的按顺序插入和删除数据时效率高。
vector: 容器随机插入删除数据时效率高。
该案例使用成员函数作为线程的入口函数;
不加任何保护措施的代码,读写的过程中可能会出错:
#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
using namespace std;
class A{
public:
//把收到的消息(玩家命令)放入到一个队列的线程入口函数
void inMsgRecvQueue(){
for(int i=0;i<100000;i++){//用数字模拟玩家发送来的命令
cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
msgRecvQueue.push_back(i);//把命令放入队列当中
}
}
//从消息队列list中读取玩家命令的线程入口函数
void outMsgRecvQueue(){
for(int i=0;i<100000;i++){
if(!msgRecvQueue.empty()){
int command=msgRecvQueue.front();//返回第一个元素
msgRecvQueue.pop_front();//取出后移除该元素
//然后处理数据
}
else
cout<<"outMsgRecvQueue执行,但是list已经空了 : "<<i<<endl;
}
cout<<"end "<<endl;
}
private:
std::list<int> msgRecvQueue;//在list中存放玩家发来的命令
};
int main(){
A myobj;
std::thread myOutMsgObj(&A::outMsgRecvQueue,&myobj);//第二个参数是引用,作用与std::ref相同,保证是子线程中使用的是主线程中的同一个对象,但是主线程后面必须等待子线程完成
std::thread myInMsgObj(&A::inMsgRecvQueue,&myobj);
myInMsgObj.join();
myOutMsgObj.join();
cout<<"主线程结束"<<endl;
return 0;
}
增加保护措施的代码,引入C++解决多线程保护共享数据的概念,“互斥量”,具体请看下一节。