17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码

17.1 C++并发与多线程-基础概念与实现
17.2 C++并发与多线程-线程启动、结束与创建线程写法
17.3 C++并发与多线程-线程传参详解、detach坑与成员函数作为线程函数
17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码
17.5 C++并发与多线程-互斥量的概念、用法、死锁演示与解决详解
17.6 C++并发与多线程-unique_lock详解
17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once
17.8 C++并发与多线程-condition_variable、wait、notify_one与notify_all
17.9 C++并发与多线程-async、future、packaged_task与promise
17.10 C++并发与多线程-future其他成员函数、shared_future与atomic
17.11 C++并发与多线程-Windows临界区与其他各种mutex互斥量
17.12 C++并发与多线程-补充知识、线程池浅谈、数量谈与总结

4.创建多个线程、数据共享问题分析与案例代码

  4.1 创建和等待多个线程

void myprint(int inum)
{
    cout << "myprint线程开始执行了,线程编号=" << inum << endl;
    //干各种事情
    cout << "myprint线程结束执行了,线程编号=" << inum << endl;
    return;
}
{
    vector <thread> mythreads;
    //创建5个线程,当然,线程的入口函数可以用同一个,这并没什么问题
    for (int i = 0; i < 5; i++)
    {
        mythreads.push_back(thread(myprint, i));  //创建并开始执行线程
    }
    for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
    {
        iter->join(); //等待5个线程都返回
    }
    cout << "main主函数执行结束!" << endl; //最后执行这句,然后整个进程退出
}

在这里插入图片描述

从结果可以看到:
  · 多个线程之间的执行顺序是乱的。先创建的线程也不见得就一定比后创建的线程执行得快,这个与操作系统内部对线程的运行调度机制有关。
  · 主线程是等待所有子线程运行结束,最后主线程才结束,所以推荐join(而不是detach)写法,因为这种写法写出来的多线程程序更容易写得稳定、健壮。
  · 把thread对象放到容器里进行管理,看起来像一个thread对象数组,这对一次性创建大量的线程并对这些线程进行管理是很方便的。

  4.2 数据共享问题分析

(1)只读的数据

    一段共享数据,如有一个容器,这里说一说容器里面的数据。如果数据是只读的,每个线程都去读,那无所谓,每个线程读到的内容肯定都是一样的。

void myprint(int inum)
{
    cout << "id为" << std::this_thread::get_id() << "的线程 打印g_v值" << g_v[0] << g_v[1] << g_v[2] << endl;
    return;
}

(2)有读有写

    事情复杂就坏在有读有写上了,如创建了5个线程,有2个线程负责往容器里写内容,3个线程负责从容器中读内容,那这种程序就要小心谨慎地写,因为代码一旦写不好就容易出问题,或者换句话说,如果写的代码不对,肯定出问题。
    最简单的处理方式是:读的时候就不能写,写的时候就不能读,两个(或者多个)线程也不能同时写,两个(或者多个)线程也不能同时读。
    请细想一下,这件事情不难理解,比如说写,写这个动作其实有很多细节步骤,如分10步,如第1步是移动指针,第2步往指针位置处写,第3步……,那这10步是一个整体,必须从头到尾把10步全做完,才能保证数据安全地写进来,所以必须用代码保证这10步都一次做完,如果写这个动作正做到第2步,突然来了个读,那可能数据就乱套了。

(3)其他案例

    这种数据共享的问题在现实生活中随处可见。例如卖火车票,若这趟火车从北京到深圳,卖这趟火车车票的售票窗口有10个(1~10号),如果1号和2号售票窗口同时发出了定20号座位票的动作。

  4.3 共享数据的保护实战范例

class A
{
public:
//把收到的消息入到队列的线程
void inMsgRecvQueue()
{
    for (int i = 0; i < 100000; i++)
    {
        cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
        msgRecvQueue.push_back(i); //假设这个数字就是我收到的命令,我直接放到消息队列里来
    }
}

//把数据从消息队列中取出的线程 
void outMsgRecvQueue()
{
    for (int i = 0; i < 100000; i++)
    {
        if (!msgRecvQueue.empty())
        {
            int command = msgRecvQueue.front();//返回第一个元素但不检查元素存在与否
            msgRecvQueue.pop_front();          //移除第一个元素但不返回					
            //这里可以考虑处理数据
            //......			
            cout << "outMsgRecvQueue()执行了,目前收消息队列中是元素:" << command << endl;
        }
        else
        {
            //cout << "outMsgRecvQueue()执行了,但目前收消息队列中是空元素" << i << endl;
        }
    }
    cout << "end" << endl;
}

private:
    std::list<int>  msgRecvQueue; //容器(收消息队列),专门用于代表玩家给咱们发送过来的命令
};
{
    A  myobja;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobja);  //注意这里第二个参数必须是引用(用std::ref也可以),才能保证线程里用的是同一个对象
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myInMsgObj.join();
    myOutnMsgObj.join();
    cout << "main主函数执行结束!" << endl;
}

    inMsgRecvQueue不断往队列中写数据,而outMsgRecvQueue不断从队列中读取和删除数据。这就叫作有读有写,如果程序员完全不控制,让这两个线程随意执行,那一定会出错,只是早一点出错或晚一点出错的问题。试想一个线程正在写还没写完,另外一个线程突然去读,或者去删除,还没删完,第一个线程又突然往里面写,这想都不用想,数据肯定乱套,程序肯定报异常。
    明白了产生问题的原因,并不难想到解决问题的办法。
    只要程序员能够确保inMsgRecvQueue线程往队列里写数据的时候,outMsgRecvQueue线程等待,等inMsgRecvQueue写完数据的时候,outMsgRecvQueue再去读和删除。或者换一种说法,只要程序员确保outMsgRecvQueue线程从队列中读数据和删除数据时,线程inMsgRecvQueue等待,等outMsgRecvQueue读和删除完数据的时候,inMsgRecvQueue再去写数据,那就保证不会出问题。
    共享数据,当某个线程操作该共享数据的时候,就用一些代码把这个共享数据锁住,其他想操作这个共享数据的线程必须等待当前操作完成并把这个共享数据的锁打开,其他线程才能继续操作这个共享数据。这样都按顺序和规矩来访问这个共享数据,共享数据就不会被破坏,程序也就不会报异常。
    这里引入C++解决多线程保护共享数据问题的第一个概念——互斥量

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值