C++11多线程第一篇:并发基本概念及实现,进程、线程基本概念

文章目录


在这里插入图片描述


在这里插入图片描述

1、并发基本概念及实现,进程、线程基本概念

1.1 并发、进程、线程的基本概念和综述

1.1.1 并发、并行

  • 两个或者更多的任务(独立的活动)同时发送(进行);一个程序同时执行多个独立的任务;

  • 以往计算机,单核cpu(中央处理器):某一个时刻只能执行一个任务:由操作系统调度,每秒钟进行多次所谓的 “任务切换” 。

  • 并发的假象(不是真正的并发);这种 “任务切换” (上下文切换)是要有时间开销的。比如操作系统要保存你切换时候的各种状态,执行信息等信息,都需要时间,一会切换回来的时候要复原这些信息。

  • 硬件发展,出现了多处理计算机:用于服务器和高性能计算机领域。台式机:在一块芯片上由多核(多个)cpu:双核、四核、八核等能狗实现真正的并发执行多个任务(硬件并发)。

  • 使用并发的原因:主要就时同时可以执行多件不同的进程,提高性能。

参照物为cpu。

并发:单个cpu在同一时间段内,多个任务同时执行,偏向于多个任务交替执行,在某一时刻其实只有一个任务在执行(单个CPU就可并发,比如时间片轮转机制)。指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的。

并行:同一时刻,多个任务同时执行(并行需要有多个CPU)。指多个cpu同时处理多个线程任务,cpu可以同时处理不同的任务,异步处理。

并发条件:

  • 第一,是否有共享变量 。
  • 第二,是否多线程环境 。
  • 第三,是否多个线程更新共享变量。

一句话:多个线程操作同一个对象。

1.1.2 可执行程序

  • 磁盘上的一个文件,windows下,一个扩展名为.exe的文件Windows下的可执行程序是.exe,但是除了.exe之外,Windows下动态库.dll和.lib也是可执行程序。linux下ls -la ,rwxrwxrwx(x就是执行权限)。
  1. 源程序: 程序可以用高级语言或汇编语言编写,用高级语言或汇编语言编写的程序称为源程序。C语言源程序的扩展名为“.c”。源程序不能直接在计算机上执行,需要用“编译程序”将源程序编译为二进制形式的代码。

  2. 目标程序: 源程序经过“编译程序”编译所得到的二进制代码称为目标程序。目标程序的扩展名为“.obj”。目标代码尽管已经是机器指令,但是还不能运行,因为目标程序还没有解决函数调用问题,需要将各个目标程序与库函数连接,才能形成完整的可执行程序。

  3. 可执行程序: 目标程序与库函数连接,形成的完整的可在操作系统下独立执行的程序称为可执行程序。可执行程序的扩展名为“.exe”(在dos/windows环境下)。

程序 是为完成某项活动所规定的方法;程序模块 即可由预处理、编译程序、汇编程序、装入程序或翻译程序作为一个整体来处理的一级独立的、可识别的程序指令。描述程序的文件称为程序文件。在C/C++中,一个程序要运行起来,要经历四个阶段:预处理、编译、汇编、链接,最后形成可执行程序。
在这里插入图片描述

预处理: C/C++的宏替换和文件包含的工作,不归入编译器的范围,而是交给独立的预处理器。主要处理源代码文件中的以“#”开头的预编译指令。处理规则为:

  • 删除所有的#define,展开所有的宏定义。
    • (注:通过这一点我们可以看到#define和const定义常量的区别,#define所做的是在预处理阶段简单地做字符串替换,不会进行任何安全检查,而const定义常量,是编译器来处理,编译器会检查是否有语法错误,比较而言,const定义常量更加安全。)
  • 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”。
  • 处理“#include”预编译指令,将文件内容替换到它的位置。比如我们这个例子中的#include <stdio.h>命令,预处理器会读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
  • 删除所有的注释,“//”和“/**/”。
  • 保留所有的#pragma编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
  • 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,编译时产生编译错误或警告能够显示行号。

c和.cpp文件经过预处理后就变成了.i(C)或.ii(C++)文件。
在这里插入图片描述

编译: 把预处理之后生成的.i(C)或.ii(C++)文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
在这里插入图片描述

汇编:汇编器根据汇编指令和机器指令的对照表,将汇编代码一 一翻译成机器码(二进制代码文件)。经汇编之后,产生可重定位的目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。

在这里插入图片描述

我们来看看可重定位的目标文件格式,Windows使用的是PE(Portable Executable)格式,Linux和Unix系统使用的是ELF(Executable and Linkable Format)格式,不管是哪种格式,基本的概念是相似的。下图是ELF可重定位目标文件的格式,一个典型的ELF可重定位目标文件包含下面几个节:
在这里插入图片描述

链接:(这里讲的链接,严格说应该叫静态链接。)

  • 链接就是将工程中的多个可重定位目标文件组合成单一可执行文件的过程。在Windows系统下可执行文件的后缀是.exe,在Unix系统下是.out,由于我的计算机使用的Windows系统,最后生成的可执行文件是test.exe。
    在这里插入图片描述

链接器必须完成两个任务:

  • 符号解析:目标文件.o定义和引用符号每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用和定义关联起来。
  • 重定位:汇编器生成地址从0开始的可重定位目标文件,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

对每个可重定位目标文件而言,它们的代码、只读数据、全局变量、静态变量等的地址都是相对于本文件而言的。在例子中的main.o、MyClass.o、MyClass2.o,它们文件中的符号(函数、全局变量、静态变量)的地址都在本文件中定好了,链接器所做的工作就是把这些文件组合成一个文件,在组合的过程中,要把每个文件中的这些符号的地址修改一下,修改的规则如下图,EIF可重定位目标文件的每个节会被映射到可执行目标文件对应的节中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGns50tX-1666522006566)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235223101.png)]

源程序们经过预处理、编译、汇编、链接最终生成了一个可执行程序。其中,预处理器、编译器、汇编器、链接器一起构成了编译系统(简称编译器)。常见的编译系统有GCC、G++、MSVC。

可执行程序是一个静态的概念,我们可以打开QQ安装目录看看QQ的可执行程序(应用程序),这个程序和我们电脑上运行着的QQ有什么关系呢?

程序保存在磁盘上,是静态的,你不去删除它它就一直都存在。运行着的QQ是一个进程,而进程运行在内存上,是动态的,你把运行着的QQ关了,那么这个进程就被操作系统回收了,这个进程就消亡了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4OlKhqN-1666522006567)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235233513.png)]

一个工程中的源程序在点击运行后,会经历预处理、编译、汇编、链接生成可执行程序,可执行程序中的某些节再被加载到内存中,最终成为进程。

1.1.3 进程

  • 运行起来的一个可执行程序,windows下是 .exe文件。在linux下是一个./文件名来运行。进程就是一个运行起来的可执行程序。
  • 进程,一个可执行程序运行起来就创建了一个进程。进程就是一个运行起来的可执行程序。
  • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
  • 进程是具有独立功能的程序在一个数据集合上运行过程,它是系统进行资源分配和调度的一个独立单位。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HOYbV76z-1666522006568)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235243681.png)]

1.1.4 线程

  • 启动一个程序就会启动一个进程(Process)。

  • 一个进程内包含1个到多个线程(Thread)(必有一个main线程,还可能有其他守护线程daemon)。

  • main线程是主线程,程序的入口,用于执行整个程序。

  • 一个线程内包含多个指令(线程是cpu的调度和执行单位)。

  • cpu是接受线程,处理指令的地方。

  • 一个cpu一次只能处理一个线程任务。

  • 线程需要获取cpu时间片才能被cpu处理。

  • 时间片很短,windows下约为15ms。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aXp4V2K-1666522006569)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235318948.png)]

  • 每个进程(执行起来的可执行程序),都由一个主线程,这个主线程是唯一的,也就是一个进程中只能有一个主线程。
  • 当你执行可执行程序,产生了一个进程后,这个主线程就随这这个进程默默的启动起来了。
  • ctrl+f5 运行这个可执行程序的时候,实际上是进程中的主线程来执行(调用)这个main函数中的代码。
  • 进程与主线程唇齿相依,有你必然有我,有我必然有你,没有我必然没有你。
  • 线程:用来执行代码的,线程可以理解成一条代码的执行通路(道路)。
  • 除了主线程之外,我们可以通过自己写代码来创建其他线程,其他线程走的是别的道路,甚至去不同的地方,我每创建一个新线程,我就可以在同一时刻,多干一个不同的事(多走一条不同的代码执行路径)。
  • 多线程(并发)
    • 线程并不是越多越好,每一个线程,都需要一个独立的堆栈空间(1M),线程之间的切换要保存很多中间状态;切换会耗费本该属于程序运行的时间。(上下文切换)。
  • 总结线程:
    • 线程是一个进程中来执行代码的,
    • 把线程这个东西理解成一条代码的执行通路(道路),一个新线程代码一条新的通路。
    • 一个进程自动包含一个主线程,主线程随着进程默默的启动并运行,我们可以通过编码来创建多个其他线程像副线程(非主线程)但是创建的数量最大都不建议超过200-300个,至于到底多少个合适,自己在实际的项目中可以不断的调整和优化。
    • 因为主线程是自动启动的,所以一个进程中最少也是有一个线程(主线程)。有进程就会有主线程。
    • 说白了:多线程程序可以同时执行不同的事务,所以运行效率高。但是到底有多高,并不是一个很容易评估和量化的东西。
    • 大家还是要在实际编程、实际项目中进行体会和调整优化。

使用多线程的案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsVZJCrJ-1666522006570)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235328671.png)]

多个线程如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-80nunPFf-1666522006571)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20221011235337548.png)]

1.1.5 程序、进程、线程

  • 程序是数据和指令的有序集合,本身无意义,就是一个静态的概念。
  • 进程是执行程序的一个运行过程,操作系统的资源分配,是动态概念。
  • 线程是程序执行过程中的确切操作的执行对象。

1.1.6 学习心得

  • 开发多线程程序,是实力的体现,一个是商用的必须要求。
  • 线程开发有一定难度。实现代码复杂,理解上更难一些,需要一定的学习时间。
  • C++线程会涉及很多新概念,对于C++道路上的成长特别关键,不要急于求成。
  • 网络通讯、网络服务器、网络方向。

1.2 线程概念讲解

1.2.1 什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列。
  • 一切进程至少都有一个执行线程。
  • 线程在进程内部运行,本质是在进程地址空间内运行。
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

在这里插入图片描述

在这里插入图片描述

1.2.2 线程的优点

在这里插入图片描述

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

1.2.3 线程的缺点

在这里插入图片描述

  • 性能损失
  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多

1.2.4 线程的异常

在这里插入图片描述

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.2.5 线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

1.3 并发的实现方法

两个或者更多的任务(独立的活动)同时发送(进行);一个程序同时执行多个独立的任务;

并发的实现手段:

  • 多个进程实现并发。
  • 在单独的进程中,创建多个线程来实现并发,创建除主线程之外的其他线程。

1.3.1 多进程并发

  • world启动后就是进程,浏览器启动后就是个进程。
  • 账号服务器、游戏逻辑服务器。服务器进程之间通信,
  • 进程之间通信(同一台电脑:管道、文件、消息队列、共享内存)。
  • 不同电脑上:网络之间的socket通信技术。

1.3.2 多线程并发

  • 单个进程中,创建多个线程。
  • 线程:轻量级进程。每个线程都右自己独立的运行路径。但是一个进程中的所有线程共享同一块地址空间。(共享内存)。
  • 全局变量,指针,引用都可以在线程之间传递。所以使用多线程开销远远小于多进程。
  • 但使用同一块内存空间会带来数据一致性问题。线程A、线程B,这要设计到加锁等问题。
  • 多线程并发和多进程并发虽然可以混合使用,但是优先考虑多线程并发而不是多进程并发,因为多线程并发效率更高。

1.3.3 进程和线程对比

在这里插入图片描述

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据
    在这里插入图片描述
  • 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境。
    在这里插入图片描述

1.3.4 多进程应用场景

在这里插入图片描述

1.3.5 总结

  • 和进程相比,线程有如下优点:

    • 线程启动速度更快,更轻量级。
    • 系统资源开销更少,执行速度更快。比如共享内存这种通信方式比任何通信方式都更快。
  • 缺点:

    • 使用起来有一定难度,比如数据一致性问题。

1.4 C++11新标准线程库

  • C++98:

    • windows:CreateThread(),_beginthread(),beginthreadexe()创建线程。

    • Linux:pthread_create()创建线程。临界区、互斥量。

    • 以前多线程代码不能跨平台。

    • 引入POSIX线程库thread(pthread)做一番配置在windows和linux上可以跨平台。

  • C++11:

    • 从C++11新标准,C++语言本身增加对多线程的支持,意味着可移植性(跨平台),这大大减少开发人员的工作量

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注