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

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(使用前将#替换为@)

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

  • 如何将 std::string& 转换为 C# 引用字符串

    我正在尝试将 C 函数转换为std string参考C 我的 API 如下所示 void GetStringDemo std string str 理想情况下 我希望在 C 中看到类似的东西 void GetStringDemoWrap r
  • 在 xaml 中编写嵌套类型时出现设计时错误

    我创建了一个用户控件 它接受枚举类型并将该枚举的值分配给该用户控件中的 ComboBox 控件 很简单 我在数据模板中使用此用户控件 当出现嵌套类型时 问题就来了 我使用这个符号来指定 EnumType x Type myNamespace
  • C# 异步等待澄清?

    我读了here http blog stephencleary com 2012 02 async and await html that 等待检查等待的看看它是否有already完全的 如果 可等待已经完成 那么该方法将继续 运行 同步
  • 类型中的属性名称必须是唯一的

    我正在使用 Entity Framework 5 并且有以下实体 public class User public Int32 Id get set public String Username get set public virtual
  • C++11 删除重写方法

    Preface 这是一个关于最佳实践的问题 涉及 C 11 中引入的删除运算符的新含义 当应用于覆盖继承父类的虚拟方法的子类时 背景 根据标准 引用的第一个用例是明确禁止调用某些类型的函数 否则转换将是隐式的 例如最新版本第 8 4 3 节
  • 如何从 Visual Studio 将视图导航到其控制器?

    问题是解决方案资源管理器上有 29 个项目 而且项目同时具有 ASP NET MVC 和 ASP NET Web 表单结构 在MVC部分中 Controller文件夹中有大约100个子文件夹 每个文件夹至少有3 4个控制器 视图完全位于不同
  • std::vector 与 std::stack

    有什么区别std vector and std stack 显然 向量可以删除集合中的项目 尽管比列表慢得多 而堆栈被构建为仅后进先出的集合 然而 堆栈对于最终物品操作是否更快 它是链表还是动态重新分配的数组 我找不到关于堆栈的太多信息 但
  • 如何使从 C# 调用的 C(P/invoke)代码“线程安全”

    我有一些简单的 C 代码 它使用单个全局变量 显然这不是线程安全的 所以当我使用 P invoke 从 C 中的多个线程调用它时 事情就搞砸了 如何为每个线程单独导入此函数 或使其线程安全 我尝试声明变量 declspec thread 但
  • 用于 FTP 的文件系统观察器

    我怎样才能实现FileSystemWatcherFTP 位置 在 C 中 这个想法是 每当 FTP 位置添加任何内容时 我都希望将其复制到我的本地计算机 任何想法都会有所帮助 这是我之前问题的后续使用 NET 进行选择性 FTP 下载 ht
  • 对类 static constexpr 结构的未定义引用,g++ 与 clang

    这是我的代码 a cp p struct int2 int x y struct Foo static constexpr int bar1 1 static constexpr int2 bar2 1 2 int foo1 return
  • WcfSvcHost 的跨域异常

    对于另一个跨域问题 我深表歉意 我一整天都在与这个问题作斗争 现在已经到了沸腾的地步 我有一个 Silverlight 应用程序项目 SLApp1 一个用于托管 Silverlight SLApp1 Web 的 Web 项目和 WCF 项目
  • 如何定义一个可结构化绑定的对象的概念?

    我想定义一个concept可以检测类型是否T can be 结构化绑定 or not template
  • C# xml序列化必填字段

    我需要将一些字段标记为需要写入 XML 文件 但没有成功 我有一个包含约 30 个属性的配置类 这就是为什么我不能像这样封装所有属性 public string SomeProp get return someProp set if som
  • 空指针与 int 等价

    Bjarne 在 C 编程语言 中写道 空指针与整数零不同 但 0 可以用作空指针的指针初始值设定项 这是否意味着 void voidPointer 0 int zero 0 int castPointer reinterpret cast
  • C# 动态/expando 对象的深度/嵌套/递归合并

    我需要在 C 中 合并 2 个动态对象 我在 stackexchange 上找到的所有内容仅涵盖非递归合并 但我正在寻找能够进行递归或深度合并的东西 非常类似于jQuery 的 extend obj1 obj2 http api jquer
  • 如何在 Android 中使用 C# 生成的 RSA 公钥?

    我想在无法假定 HTTPS 可用的情况下确保 Android 应用程序和 C ASP NET 服务器之间的消息隐私 我想使用 RSA 来加密 Android 设备首次联系服务器时传输的对称密钥 RSA密钥对已在服务器上生成 私钥保存在服务器
  • 使用特定参数从 SQL 数据库填充组合框

    我在使用参数从 sql server 获取特定值时遇到问题 任何人都可以解释一下为什么它在 winfom 上工作但在 wpf 上不起作用以及我如何修复它 我的代码 private void UpdateItems COMBOBOX1 Ite
  • 对于某些 PDF 文件,LoadIFilter() 返回 -2147467259

    我正在尝试使用 Adob e IFilter 搜索 PDF 文件 我的代码是用 C 编写的 我使用 p invoke 来获取 IFilter 的实例 DllImport query dll SetLastError true CharSet
  • DotNetZip:如何提取文件,但忽略zip文件中的路径?

    尝试将文件提取到给定文件夹 忽略 zip 文件中的路径 但似乎没有办法 考虑到其中实现的所有其他好东西 这似乎是一个相当基本的要求 我缺少什么 代码是 using Ionic Zip ZipFile zf Ionic Zip ZipFile
  • Mono 应用程序在非阻塞套接字发送时冻结

    我在 debian 9 上的 mono 下运行一个服务器应用程序 大约有 1000 2000 个客户端连接 并且应用程序经常冻结 CPU 使用率达到 100 我执行 kill QUIT pid 来获取线程堆栈转储 但它总是卡在这个位置

随机推荐

  • 如何存储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