LV05-02-线程-01-线程的基础知识

本文主要是进程——线程的基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、并发与并行

了解线程之前,需要先了解一下并发和并行的概念。

1. 串行

这个我们应该并不陌生,它指的是一种顺序执行,例如先完成 task1 ,接着做 task2 、直到完成 task2 ,然后做 task3 、直到完成 task3 …… 依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。

image-20220523215917482

2. 并行

并行指的是可以并排或者说并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,例如并行运行 task1 、 task2 、 task3 。

image-20220523220437920

并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着就可以了称之为并行。

3. 并发

并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉或者说交替执行),这就是并发运行。

image-20220523221252531

二、线程

1. 什么是线程?

在早期的操作系统中都是以进程作为独立运行的基本单位,直到后面,计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程。

线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。例如某应用程序设计了两个需要并发运行的任务 task1 和 task2 ,可将两个不同的任务分别放置在两个线程中。

在计算机操作系统中,通常同时运行着几十上百个不同的线程,在单核或多核处理系统中都是这样,对于单核处理器系统来说,它只有一个执行单元,同时只能执行一条指令,只能采用并发运行系统中的线程,而肯定不会是是串行,而事实上确实如此。内核实现了调度算法,用于控制系统中所有线程的调度,简单点来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。

对于多核处理器系统来说,它拥有多个执行单元,可以并行执行多条指令,在操作系统中,多个执行单元以并行方式运行多个线程,同时每一个执行单元以并发方式运行系统中的多个线程。

计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替或者交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程

2. 它是如何创建的?

当一个程序启动时,就有一个进程被操作系统( OS )创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程( Main Thread ),因为它是程序一开始时就运行的线程。应用程序大多都是以 main() 做为入口开始运行的,所以 main() 函数就是主线程的入口函数, main() 函数所执行的任务就是主线程需要执行的任务。

所以任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,例如在前边学习进程的时候写的应用程序都是单线程程序,它们只有主线程;既然有单线程进程,那自然就存在多线程进程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用 pthread_create 创建一个新的线程,后边再说),那么创建的新线程就是主线程的子线程。

主线程的重要性体现在两方面:

  • 其它新的线程(也就是子线程)是由主线程创建的;
  • 主线程通常会在最后结束运行,执行各种清理工作,例如回收各个子线程。

3. 线程的特点

线程是程序最基本的运行单位,当一个进程开始运行的时候,真正运行的其实是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。

同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈( call stack ,我们称为线程栈),自己的寄存器环境( register context )、自己的线程本地存储( thread-local storage )。

点击查看一个进程中的多个线程的共享资源和私有资源
  • 共享资源通常有:可执行的指令、静态数据、进程中打开的文件描述符、当前工作目录、用户 ID 和用户组 ID 等。
  • 私有资源通常有:线程 ID ( TID )、 PC (程序计数器)和相关寄存器、堆栈、错误号 ( errno )、优先级、执行状态和属性等

在多线程的应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被 CPU 执行,一般具有以下特点:

(1)线程不单独存在、而是包含在进程中;

(2)线程是参与系统调度的基本单位;

(3)可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;

(4)共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。

【注意】线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位

三、为什么要使用多线程?

进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程)并发处理多任务的需求,那我们究竟选择哪种处理方式更合适呢?

  • 多进程编程的劣势

(1)进程间切换开销大。多个进程同时运行(指宏观上同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。

(2)进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦(后边会学习到)。

  • 多线程的优势

(1)同一进程的多个线程间切换开销比较小,将大大提高任务切换的效率。

(2)同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易,同时避免了额外的 TLB & cache 的刷新。

(3)线程创建的速度远大于进程创建的速度。

多线程编程相比于多进程编程的优势是比较明显的,在实际的应用当中多线程远比多进程应用更为广泛。但是多线程也有它的缺点、劣势。例如多线程编程难度高,因为在多线程环境下需要考虑很多的问题,例如线程安全问题、信号处理的问题等,编写与调试一个多线程程序比单线程程序困难得多。所以也会有很多多进程的编程模型,多进程的编程通常会用在一些大型应用程序项目中,像网络服务器应用程序,在中小型应用程序中用的会比较少。

四、线程的上下文切换

学习进程的时候了解了一下进程的上下文切换,线程也是有上下文切换的。所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。对于线程和进程,我们可以这么理解:

  • 当进程只有一个线程时,可以认为进程就等于线程;
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;但是,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

所以线程的上下文切换,还需要看线程是否属于同一个进程:

  • 当多个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当多个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;

五、线程 ID

就像每个进程都有一个进程 ID 一样,每个线程也有其对应的标识,称为线程 ID 。进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。

1. pthread_self()

1.1 函数说明

在 linux 下可以使用 man pthread_self 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
pthread_t pthread_self(void);

【函数说明】该函数用于获取调用该函数的线程的线程 ID 。

【函数参数】 none

【返回值】 pthread_t 类型,这个函数总是成功的,返回调用线程的 ID 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_t tid;
tid = pthread_self();

【注意事项】 使用 printf() 打印数据的时候,格式应该使用 lu 。

1.2 使用实例

后边的例子中自然会用到。

2. pthread_equal()

2.1 函数说明

在 linux 下可以使用 man pthread_equal 命令查看该函数的帮助手册。

1
2
3
/* Compile and link with -pthread. */
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

【函数说明】该函数用于比较两个线程 ID 是否相等。

【函数参数】

  • t1 : pthread_t 类型,线程的线程 ID 。
  • t2 : pthread_t 类型,需要比较的另一个线程的线程 ID 。

【返回值】 int 类型,这两个线程相等返回非 0 值,不相等返回 0 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <pthread.h>

/* 至少应该有的语句 */
pthread_equal(tid1, tid2);

【注意事项】 none

2.2 使用实例

后边的例子自然会用到。