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

2023-11-07

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

创建和等待多个线程

在实际的工作中,可能要创建的线程不止一个,也许有多个。所以,这里展示一下创建多个线程的一种写法,大家可以举一反三。

lesson4.cpp的上面位置,书写线程入口函数 myprint

void myprint(int inum)
{
    cout << "myprint线程开始执行了,线程编号=" << inum << endl;
    // 干各种事情
    cout << "myprint线程结束执行了,线程编号=" << inum << endl;
    return;
}

main主函数中,加入如下代码:

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; // 最后执行这句,然后整个进程退出

执行起来,看一看结果(由于多个线程无序输出,结果看起来比较乱):

myprint线程开始执行了,线程编号=myprint线程开始执行了,线程编号=2
myprint线程结束执行了,线程编号=2
myprint线程开始执行了,线程编号=3
myprint线程结束执行了,线程编号=3
myprint线程开始执行了,线程编号=1
myprint线程结束执行了,线程编号=1
0
myprint线程结束执行了,线程编号=0
myprint线程开始执行了,线程编号=4
myprint线程结束执行了,线程编号=4
main主函数执行结束!

从结果可以看到:

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

数据共享问题分析

只读的数据

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

例如有一个全局的容器:

vector<int> g_v = {1, 2, 3};

main 主函数中依旧是上面这样创建5个线程,每个线程都打印容器g_v中的元素值, main中代码不需要修改,只需要修改线程入口函数myprint中的代码。修改为如下:

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

执行起来,虽然结果看起来比较乱,但其实程序执行的是稳定和正常的。

有读有写

事情坏就坏在有读有写上了,如创建了5个线程,有2个线程负责往容器里写内容,3个线程负责从容器中读内容,那这种程序就要小心谨慎地写,因为代码一旦写不好就容易出问题,或者换句话说,如果写的代码不对,肯定出问题。

最简单的处理方式是:读的时候就不能写,写的时候就不能读,两个(或者多个)线程也不能同时写,两个(或者多个)线程也不能同时读。

请细想一下,这件事情不难理解,比如说写,写这个动作其实有很多细节步骤,如分10步,如第1步是移动指针,第2步往指针位置处写,第3步⋯⋯,那这10步是一个整体,必须从头到尾把10步全做完,才能保证数据安全地写进来,所以必须用代码保证这10步都一次做完,如果写这个动作正做到第2步,突然来了个读,那可能数据就乱套了。可能正好就读到了正在写还没写完的数据等,那么,各种诡异和不可预料的事情就会发生,一般的表象就是程序会立即运行崩溃。

其他案例

这种数据共享的问题在现实生活中随处可见。例如卖火车票,若这趟火车从北京到深圳,卖这趟火车车票的售票窗口有10个(1~10号),如果1号和2号售票窗口同时发出了定20号座位票的动作,那么肯定不能这两个窗口都订成功这张票(一个座位不可能卖给两个人),肯定是一个窗口订票成功,另一个窗口订票失败。那么,订票这个动作至少要分成两个小步骤:

  1. 订票系统要先看这个座位是否已经被其他人订了(这是一个读操作),如果订过了,订票系统就直接告诉售票窗口“订票失败”,如果这个座位没被订,就继续进行下面的第二步。
  2. 订票系统帮助售票窗口订这个座位的票、设置这个座位的状态为已经被订状态、记录被哪个售票窗口所订、订的时间等(这是一个写操作),然后返回订票成功信息给售票窗口(注意这一步中的所有这些动作都要一次完成,中间不能被截断)。

那请想一想,1号售票窗口订票的时候,其他售票窗口想订票,那也必须得等着,等1号售票窗口做完了订票这个动作之后,其他售票窗口才能继续订票,否则,就有可能两个人都订到了20号座位(同一个座位)的票,那这两个人就得打架了。

共享数据的保护实战范例

这里举一个实际工作中能够用到的范例来讲解共享数据的保护问题。

就以一个网络游戏服务器开发为例来说明。这里把问题简化,假设现在在做一个网络游戏服务器,这个网络游戏服务器程序包含两个线程,其中一个线程用来从玩家那里收集发送来的命令(数据),并把这些数据写到一个队列(容器)中,另一个线程用来从这个队列中取出命令,进行解析,然后执行命令对应的动作。

这里就假设玩家每次发送给本服务器程序的是一个数字,这个数字就代表一个玩家发送过来的命令。

定义一个队列,这里使用list容器。list容器与vector容器类似,只是list在频繁按顺序插入和删除数据时效率更高,而vector容器随机插入和删除数据时效率比较高。如果想更详细地了解list容器,可以借助搜索引擎学习。

下面开始写这个服务器程序。

lesson4.cpp的开始位置增加如下#include语句:

#include <list>

同时,这里准备使用类的成员函数作为线程入口函数的方法来书写线程。类A的定义如下(代码写在MyProject. cpp文件的上面位置):

class A
{
public:
    // 把收到的消息入到队列的线程
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
            msgRecvQueue.push_back(i); // 假设这个数字就是收到的命令,则将其直接放到消息队列里
        }
    }
    // 把数据从消息队列中取出的线程
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 100000; i++)
        {
            if (!msgRecvQueue.empty())
            {
                int command = msgRecvQueue.front(); // 返回第一个元素但不检查元素存在与否
                msgRecvQueue.pop_front();           // 移除第一个元素但不返回
                // 这里可以考虑处理数据
                // ……
            }
            else
            {
                cout << "outMsgRecvQueue()执行了,但目前收消息队列中是空元素" << i << endl;
            }
        }
        cout << "end" << endl;
    }

private:
    std::list<int> msgRecvQueue; // 容器(收消息队列),专门用于代表玩家给咱们发送过来的命令
};

main主函数中,代码调整成如下的样子:

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再去写数据,那就保证不会出问题。

所以这里面我们看到了这个队列:

std::list<int>msgRecvQueue; //队列,也是容器

就是所说的共享数据,当某个线程操作该共享数据的时候,就用一些代码把这个共享数据锁住,其他想操作这个共享数据的线程必须等待当前操作完成并把这个共享数据的锁打开,其他线程才能继续操作这个共享数据。这样都按顺序和规矩来访问这个共享数据,共享数据就不会被破坏,程序也就不会报异常。

现在抛出了问题,并给出了解决这个问题的初步想法,那具体该怎样解决问题?如何把这个初步的解决问题的想法代码化呢?

这里引入C++解决多线程保护共享数据问题的第一个概念——互斥量。这是一个非常重要的概念,请现在开始强化记忆这个词。

代码地址

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

创建多个线程、数据共享问题分析与案例代码 的相关文章

  • WindowsError:[错误 126] 使用 ctypes 加载操作系统时

    python代码无法在Windows 7平台上运行 def libSO lib ctypes cdll LoadLibrary ConsoleApplication2 so lib cfoo2 1 3 当我尝试运行它时 得到来自python
  • 在 CPP 类中将 C 函数声明为友元

    我需要在 C 函数中使用类的私有变量 我正在做这样的事情 class Helper private std string name public std getName return name friend extern C void in
  • 如何在类文件中使用 Url.Action() ?

    如何在 MVC 项目的类文件中使用 Url Action Like namespace 3harf public class myFunction public static void CheckUserAdminPanelPermissi
  • 如何将 SOLID 原则应用到现有项目中

    我对这个问题的主观性表示歉意 但我有点卡住了 我希望之前处理过这个问题的人能够提供一些指导和建议 我有 现在已经成为 一个用 C 2 0 编写的非常大的 RESTful API 项目 并且我的一些类已经变得巨大 我的主要 API 类就是一个
  • 有些有助于理解“产量”

    在我不断追求少吸的过程中 我试图理解 产量 的说法 但我不断遇到同样的错误 someMethod 的主体不能是迭代器块 因为 System Collections Generic List 不是迭代器接口类型 这是我被卡住的代码 forea
  • 如何将 .txt 文件中的数据转换为 xml? C#

    我在一个文本文件中有数千行数据 我想通过将其转换为更容易搜索的内容来轻松搜索 我希望 XML 或其他类型的大型数据结构 尽管我不确定它是否是最好的对于我的想法 每行的数据如下所示 第 31 册 托马斯 乔治 32 34 154 每本书都不是
  • 强制初始化模板类的静态数据成员

    关于模板类的静态数据成员未初始化存在一些问题 不幸的是 这些都没有能够帮助我解决我的具体问题的答案 我有一个模板类 它有一个静态数据成员 必须为特定类型显式实例化 即必须专门化 如果不是这种情况 使用不同的模板函数应该会导致链接器错误 这是
  • cpp.react库的C++源代码中奇怪的“->* []”表达式

    这是我在文档中找到的 C 片段cpp react 库 https github com schlangster cpp react implicit parallelism auto in D MakeVar 0 auto op1 in g
  • 如何使用 Regex.Replace 从字符串中删除数字?

    我需要使用Regex Replace从字符串中删除所有数字和符号 输入示例 123 abcd33输出示例 abcd 请尝试以下操作 var output Regex Replace input d string Empty The d标识符
  • 在 C# 中检查 PowerShell 执行策略的最佳方法是什么?

    当你跑步时Get ExecutionPolicy在 PowerShell 中 它得到有效的执行政策 https learn microsoft com en us powershell module microsoft powershell
  • 在 VS 中运行时如何查看 C# 控制台程序的输出?

    我刚刚编写了一个名为 helloworld 的聪明程序 它是一个 C NET 4 5 控制台应用程序 在扭曲的嵌套逻辑迷宫深处 使用了 Console WriteLine 当我在命令行运行它时 它会运行并且我会看到输出 我可以执行其他命令并
  • 如何使用 x64 运行 cl?

    我遇到了和这里同样的问题致命错误 C1034 windows h 未设置包含路径 https stackoverflow com questions 931652 fatal error c1034 windows h no include
  • 在 C 中使用枚举而不是 #defines 作为编译时常量是否合理?

    在 C 工作了一段时间后 我将回到 C 开发领域 我已经意识到 在不必要的时候应该避免使用宏 以便让编译器在编译时为您做更多的工作 因此 对于常量值 在 C 中我将使用静态 const 变量或 C 11 枚举类来实现良好的作用域 在 C 中
  • C++ - 多维数组

    处理多维数组时 是否可以为数组分配两种不同的变量类型 例如你有数组int example i j 有可能吗i and j是两种完全不同的变量类型 例如 int 和 string 听起来您正在寻找 std vector
  • 将函数参数类型提取为参数包

    这是一个后续问题 解包 元组以调用匹配的函数指针 https stackoverflow com questions 7858817 unpacking a tuple to call a matching function pointer
  • 模板类的模板构造函数的 C++ 显式模板特化

    我有一个像这样的课程 template
  • C++ 对象用 new 创建,用 free() 销毁;这有多糟糕?

    我正在修改一个相对较大的 C 程序 不幸的是 并不总是清楚我之前的人使用的是 C 还是 C 语法 这是在一所大学的电气工程系 我们 EE 总是想用 C 来做所有事情 不幸的是 在这种情况下 人们实际上可以逃脱惩罚 但是 如果有人创建一个对象
  • Visual Studio 2015:v120 与 v140?

    仅供参考 Win10 x64 我今天开始尝试 Visual Studio 2015 在弄清楚如何运行 C C 部分后 我尝试加载一个大型个人项目 该项目使用非官方的glsdk http glsdk sourceforge net docs
  • 了解 Lambda 表达式和委托 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我已经尝试解决这个问题很长一段时间了 阅读在线博客和文章 但到目前为止还没有成功 什么是代表 什么是 Lambda 表达式 两者的优点
  • 为什么空循环使用如此多的处理器时间?

    如果我的代码中有一个空的 while 循环 例如 while true 它将把处理器的使用率提高到大约 25 但是 如果我执行以下操作 while true Sleep 1 它只会使用大约1 那么这是为什么呢 更新 感谢所有精彩的回复 但我

随机推荐

  • 如何存储Ajax请求的响应值

    如何存储Ajax请求的响应值 一 背景 二 代码部分 三 总结 一 背景 开发者使用Ajax请求网络 获取到了返回的结果 但开发者不想将回调函数写的过于冗长 因此希望将Ajax请求的返回值存储到一个变量中 方便后期取用 二 代码部分 代码部
  • Qt简介以及工程创建

    Qt是一种跨平台应用程序和UI开发框架 只需要一次性开发应用程序 可应用于不同的系统 Qt不是一个严格的前后端 而是一种框架 Qt Creator是一种全新跨平台 Qt IDE集成开发环境 可以单独使用 也可以与Qt库和开发工具组成一套完整
  • Unhandled exception at 0x00291422 in x.exe: 0xC0000005: Access violation writing location 0x37ACCE08

    源码如下 include
  • 线性表顺序存储の介绍、应用 与 实践(第二章: 线性表 )

    一 线性表的介绍 线性表 线性表 linear list 是n个具有相同特性的数据元素的有限序列 线性表是一种在实际中广泛使用的数据结构 常见的线性表 顺序表 链表 栈 队列 字符串 线性表在逻辑上是线性结构 也就说是连续的一条直线 但是在
  • 代码覆盖度工具OpenCppCoverage(cpp)、EclEmma(java)、Coverage(python)使用

    一 OpenCppCoverage cpp OpenCppCoverage是一个运行在windows上的程序 其不是在编译时进行插桩 而是在运行时 因此保证了代码和测试的一致性 参考文档 https github com OpenCppCo
  • 【c/c++】#pragma once 与 #ifndef 的区别解析

    原文地址 http blog csdn net hkx1n article details 4313303 作用 为了避免同一个文件被include多次 C C 中有两种方式 一种是 ifndef方式 一种是 pragma once方式 在
  • 智能指针使用陷阱

    智能指针使用陷阱 1 不能把一个原生指针交给多个智能指针管理 int x new int 10 unique ptr
  • android同步目录,如何使用FolderSync在安卓手机上同步文件夹到坚果云?

    FolderSync 是一款Android 端的文件同步工具 可以将手机中的文件自动同步到云端空间或者PC端 支持包括 FTP WebDAV Dropbox Amazon S3 FTP FTPS SFTP WebDAV等 可以使用WebDA
  • 几种图像的分割方法汇总

    图像分割指的是将原图像按照灰度 纹理 颜色 形状等划分成不同的区域 因此 在同一个区域间 呈现出具备一些相同的特点 而在不同的区域间 分割出的各个图像会有一定的差别 1 基于阈值的分割方法 基于阈值的分割方法是按照原图像的灰度特征划分出一个
  • k8s——kubectl

    目录 一 k8s管理操作方法 二 陈述式资源管理方法 1 基本信息查看 1 1 查看k8s版本信息 1 2 查看资源对象简写 1 3 查看集群信息 1 4 配置kubectl自动补全 1 5 node节点查看日志 2 基本信息查看 2 1
  • 大数据高频面试题【目录】

    大数据高频面试题 目录 小标题既超链接 点击可直接跳转 手写代码 Linux Shell Hadoop Flume Kafka Hive HBase Sqoop Scala Spark 项目中的常见问题 JavaSE Redis MySQL
  • LM393 电压比较器及其典型电路介绍

    这几天都在看一些开源项目 好多代码都没有什么注释 看烦了 看看小芯片吧 LM393 主要用途 限幅器 脉冲发生器 方波发生器 延时发生器 数字逻辑门电路 多频振荡器等等 引脚分布图 等效电路图 同相端电压大于反向端电压时 输出高电平 同相端
  • 【python】使用matplotlib绘制柱状图

    在制作图标时需要绘制柱状图 下面对其进行了绘制 主要就是使用matplotlib的bar函数 bar函数官方API为 matplotlib pyplot bar 下面是从一篇论文中随意截取的一段话 进行后续的绘制图像 import matp
  • 服务器故障排查方法总结

    服务器故障排查方法总结 问题描述 查找步骤 1 查找top检查服务器负载是否有问题 2 在服务器中查看网站的访问记录 3 这个时候先对数据库进行重启 对apache进行重启 4 查找数据库错误日志 问题描述 每当出现网站访问不了的时候 估计
  • Java list修改某个元素值的方法

    修改list中下标为index对象的值 set index element 增添 add index element
  • 在Android上修改读取IMEI码的方法

    我们知道 如果是移动设备 厂家都提供了IMEI码写入及读出的方法 但由于我们的是非移动设备 可是我们也需要写入IMEI码 供第三方的软件读取 如正版地图等 我们找到frameworks base telephony java android
  • selenium 隐式等待如何使用_selenium 中使用等待的三种方法

    现在很多的 web 网站使用 AJAX 技术 当页面加载到浏览器 这个页面的很多元素显示出来的可能不一致 如果一个元素还未加载出来 在定位的时候 就会抛出异常 ElementNotVisibleException 这个时候就要使用等待方法解
  • 【转】latex常见错误对照表

    原文链接 http www cs utexas edu witchel errorclasses html Latex Error Classes Ambiguous Errors This is a list of error class
  • 【低功耗蓝牙】① 蓝牙广播数据格式分析

    摘要 本文章主要讲解了蓝牙的发展史 蓝牙信号 蓝牙广播数据的格式 最后使用ESP32芯片MicroPython固件给出了蓝牙广播的具体代码 是蓝牙初学者很好的参考资料 也可以参考下我在B站的蓝牙视频教程 ESP32教程 第二章 低功耗蓝牙B
  • 创建多个线程、数据共享问题分析与案例代码

    创建多个线程 数据共享问题分析与案例代码 创建和等待多个线程 在实际的工作中 可能要创建的线程不止一个 也许有多个 所以 这里展示一下创建多个线程的一种写法 大家可以举一反三 在lesson4 cpp的上面位置 书写线程入口函数 mypri