[Linux] 多线程概念相关分析: 什么是线程、再次理解进程、线程的创建与查看、线程异常、线程与进程的对比...
博主有关Linux进程相关介绍的文章:
Linux线程概念
与平台有关
什么是线程
- 线程是在进程内部运行的执行流
- 线程相比进程, 粒度更细, 调用成本更低
- 线程是CPU调度的基本单位
PCB(task_struct)中描述着进程地址空间, 进程地址空间与物理内存 通过两张页表来相互映射.
通过对fork()返回值的判断, 让父子进程执行不同的代码块
.不同的执行流, 可以做到执行不同的资源, 即 可以做到对特定资源的划分
只创建 PCB
. 将新的PCB 指向已经存在的进程.在Linux操作系统中
, 我们就 可以将这样的PCB执行流称作 "线程"
.这里只是介绍了一下Linux操作系统中, 线程的
粗粒度
的实现原理
线程 : 进程 = N : 1
PCB
将进程的所有属性 描述、组织、管理起来, 那么对线程, 毫无疑问也是需要用一个结构体描述、组织、管理起来的. 在大多数的操作系统中, 描述线程的结构体
叫做 TCB
PCB
和 TCB
. 那么 PCB
和 TCB
之间一定存在非常复杂的耦合关系. 因为 PCB
描述一个进程, 而 TCB
描述进程内部的线程. 这两部分一定存在相当一部分的重叠属性, 还有一定的包含关系.在Linux操作系统下
.不同操作系统实现线程的方式可能是不同的
.Linux操作系统
就没有另外实现一个描述线程的结构体, 而是 用task_struct(进程PCB)模拟了线体
. 即Linux操作系统中, 描述进程和描述线程的结构体实际上是同一个结构体: task_struct
真·线程 操作系统
Win的开发者认为进程和线程在执行流层面是不同的东西
. 进程有自己的执行流, 线程在进程内部也有自己的执行流进程和线程在概念上不做区分
, 都是执行流
. PCB要不要被CPU调度?TCB要不要被CPU调度?PCB调度要不要优先级?TCB要不要?要不要通过PCB找到代码和数据?要不要通过TCB找到代码和数据?进程切换要不要保护进程的上下文数据?线程切换要不要保护上下文数据?……Linux线程, 其实就使用task_struct(进程PCB)模拟实现的.
只不过, 线程的TCB(实际上还是PCB)只能访问执行 整个进程中的一小块的代码和数据
这样做有什么好处?
用进程PCB模拟实现线程, 对线程
可以复用操作系统中已经针对进程实现的各种调度算法
, 因为进程和线程的描述结构是相同的.也不用维护进程和线程之间的关系.
也就是说, Linux操作系统中
线程TCB底层就可以看作进程PCB
Linux进程-再理解
CPU看到的所有task_struct都是一个进程
CPU看到的所有task_struct都是一个执行流
进程是 承担操作系统资源分配的基本实体
. 即 进程是 向系统申请资源的基本单位
我们可以称 只有一个执行流的进程 为 但单执行流进程, 称 内部存在多个执行流的进程 为 多执行流进程
体量要小
线程
, 可以看作是 轻量化的进程
线程, 是CPU调度的基本单位
-
线程是在进程内部运行的执行流
线程只访问执行进程的一部分数据和代码
-
线程相比进程, 粒度更细, 调用成本更低
进程切换调度, 需要切换PCB、进程地址空间、页表等
而线程切换调度, 只需要切换TCB(实际还是PCB)就可以
-
线程是CPU调度的基本单位
Linux线程的创建、查看
pthread_create
和 pthread_join
int pthread_create(pthread_t *thread, const pthread_attr *attr, void *(*start_routine)(void *), void *arg);
pthread_t
就是一个无符号长整型:nullptr
空指针
参数为空指针
的 函数指针, 用于 传入此线程需要执行的函数
.第三个参数(函数指针)所指向的函数的参数
int pthread_join(pthread_t thread, void **retval);
- 第一个参数传入 需要等待的线程的id
- 第二个参数接收退出结果, 暂时不关心. 我们只是看一看现象
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;
void* threadFun1(void* args) {
string str = (char*)args;
while (true) {
cout << str << ": " << getpid() << " " << endl;
sleep(1);
}
}
void* threadFun2(void* args) {
string str = (char*)args;
while (true) {
cout << str << ": " << getpid() << " " << endl;
sleep(1);
}
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, threadFun1, (void*)"thread_1");
pthread_create(&tid2, nullptr, threadFun2, (void*)"thread_2");
sleep(1);
while (true) {
cout << " 主线程运行: " << getpid() << " " << endl;
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
pthread_create()
接口, 编译连接时需要链接 pthread 库输出结果可能会混乱, 可能与线程的优先级与CPU核心、线程数有关
输出结果混乱, 也说明了线程可以并行运行
ps -aL
命令 来查看线程(a: all, -L: 轻量级进程):三个线程同时属于一个PID 23412. 还拥有各自的 LWP 轻量级进程编号
.有一个线程的LWP与PID相同, 表示此线程是主线程
有兴趣的话, 可以在创建两个线程之后, 再创建一个子进程.
创建之后, 观察子进程有没有什么地方与之前创建的子进程时的情况不同
线程相关概念总结
- 线程是程序内部的一个执行线路, 更准确一点的定义是:
线程是进程内部的控制序列
- 一切进程,
至少都有一个线程
- 线程在进程内部运行, 本质是在进程地址空间内运行
- 在Linux操作系统中,
CPU看到的PCB都比传统意义的PCB要轻量化
. 因为Linux中的PCB可能表示的只是一个线程 - 透过进程地址空间是可以看到进程的,
将进程资源合理的分配给每一个进程内部的执行流, 就形成了线程执行流
thread_struct{}
结构体内部存储的大部分都是寄存器相关信息. 与维护不同线程的上下文数据有关系Linux内核源码中, 有关于task_struct内部的成员其实我们已经可以看懂一部分了. 可以尝试去分辨一下成员都代表什么
线程的优点
-
创建一个新线程的成本比创建一个新进程的成本小的多
创建一个新进程, 操作系统需要分别创建PCB、进程地址空间、页表, 如果对数据做了修改还需要写时拷贝等
而创建一个新线程, 则只需要创建一个PCB就可以了, 进程地址空间、页表、数据等都直接使用原进程的就可以
-
与进程之间的切换相比, 线程之间的切换需要操作系统做的工作也会少很多
如果CPU需要切换进程运行, 那么不仅需要切换PCB还需要切换页表等诸多的数据
而切换线程的话, 就只需要切换PCB就可以了
-
线程占用资源比进程要少很多
还是那个原因, 多线程是公用一个进程地址空间和同一页表运行的, 而每个进程都拥有自己的进程地址空间和页表
-
对于计算密集型应用, 为了能在多处理器系统上运行, 会将计算分解到多线程去实现
比如文件加密应用, 可以用多线程将加密工作拆分, 加密完成之后再将文件合并, 就可以完成加密
-
对于I/O密集型应用, 为了提高性能, 将I/O操作重叠. 线程可以同时等待不同的I/O操作
比如一个程序运行时, 需要等待操作系统和网卡之间的I/O操作, 又要等待操作系统和磁盘之间的I/O操作.
如果单线程的话, 这两个I/O操作只能一个一个等, 不过, 如果是多线程的话就可以同时等待不用排队.
一般来说, 有多少CPU就可以支持多少线程同时工作.
不过现在CPU都可以模拟多线程, 一个CPU也可能模拟出多线程.
线程的缺点
-
可能造成性能损失
比如一个密集计算型线程正在运行, 且很少或不会被其他外部事件阻塞. 那么这类线程往往是无法与其他线程共用一个CPU的.
如果密集计算型线程的数量比CPU支持的多线程数量还要多, 这些线程就可能不停的被CPU调度:不停的换出、换入. 因为这些线程都是要运行一下的, 不会只照着一部分线程一直运行, 而是会这一部分执行执行、那一部分执行执行. 这就会因为不停调度而造成性能损失.
最好线程不要太多.
-
健壮性低
如果是进程, 由于进程地址空间的存在 进程是非常健壮的, 一个进程再怎么运行如果不是刻意为之一般也无法影响另一个进程.
一个多线程程序内, 可能会因为 时间分配的细微偏差、共享了某些不该共享的数据, 而对其他线程或整个程序造成很大的不良影响.
-
缺乏访问控制
操作系统中, 进程是访问控制的基本粒度, 因为进程具有独立性. 多线程访问可能会同时访问同一个数据, 而且很有可能出大问题
-
编程难度高
线程异常
线程出现异常是会影响整个进程
的. 线程出现异常
其实就是进程出现了异常
.Linux进程 VS 线程
-
进程是系统资源分配的基本单位
-
线程是调度的基本单位
-
多线程共享进程数据, 不过不同线程也有自己的一部分数据:
-
线程ID
就像每个进程都有自己的ID一样, 每个线程也都有自己的ID
-
一组寄存器
每个线程都有一组寄存器, 用来维护线程的上下文数据
-
线程栈
进程在运行时, 都会有自己的栈结构, 来给函数的压栈、临时变量等数据提供空间
其实每个线程也都会维护自己的栈区, 因为线程也可能会不停的函数调用等操作. 所以是需要维护自己的栈区的.
-
errno
-
信号屏蔽字
上面介绍信号异常时提到, 线程异常 就是 进程异常. 线程异常操作系统会向线程发送信号.
不过线程是与进程共享信号处理方法的, 所以一般情况下线程异常 也就是进程异常
不过, 虽然线程与进程共享信号处理方法, 但是线程是有自己的信号屏蔽字的.
也就是说, 操作系统向线程和进程发送同一信号, 可能进程会递达, 而线程却会阻塞.
调度优先级
-
-
线程和进程会共享这些资源:
-
代码和数据
进程中定义的函数, 每个线程都可以调用. 进程中定义的全局变量, 每个线程也都可以访问
-
文件描述符表
虽然 文件描述符表并不是进程地址空间内的数据 而是内核数据(在PCB中维护)
但是 进程的文件描述符表 也是与线程共享的, 线程PCB会指向主线程PCB的文件描述符表
-
信号的处理方法
-
进程当前运行路径
-
用户ID和组ID
-
作者: 哈米d1ch 发表日期:2023 年 4 月 11 日