c++11并发与多线程【第四节】创建和等待多个线程

一、创建和等待多个线程

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++解决多线程保护共享数据的概念,“互斥量”,具体请看下一节。

文章内容来源《C++并发与多线程视频课程》

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信