【linux】多线程概念详述

文章目录

  • 一、线程基本概念
    • 1.1 进程地址空间与页表
    • 1.2 页表结构
    • 1.3 线程的理解
      • 1.3.1 如何描述线程
    • 1.4 再谈进程
    • 1.5 代码理解
      • 1.5.1 原生库提供线程pthread_create
    • 1.6 资源共享问题
    • 1.7 资源私有问题
  • 二、总结
    • 2.1 什么是线程
    • 2.2 并行与并发
    • 2.3 线程的优点
    • 2.4 线程的缺点
    • 2.5 线程异常
    • 2.6 进程与线程间的关系

一、线程基本概念

1.1 进程地址空间与页表

在这里插入图片描述

注意这里的页表部分:

在上一章【linux】进程信号——信号的保存和处理中我们讲了页表有用户级页表和内核级页表。如图其实页表还有其他很多属性。

举个例子:当我们对常量区的数据进行修改时,为什么会报错呢?

OS会先通过页表找到物理地址,然后查RWX权限,发现只有R权限,所以地址转换单元MMU会硬件报错,转化成信号,终止进程(段错误)。

而经过U/K权限的时候,如果是U就直接访问,是K就会去CPU查看当前的运行级别,是内核级别才能访问带K的映射关系。

那么该如何看待进程地址空间和页表呢?

1️⃣ 进程地址空间(虚拟内存)是进程能够看到的资源窗口。因为能看到的资源都是通过进程地址空间让我们看到的。
2️⃣ 页表是决定进程真正拥有资源的情况
3️⃣ 合理的对进程地址空间+页表进行资源划分,就可以对进程的所有资源进行分类

进程地址空间一共有2^32个地址。那么按道理页表也应该有2^32个条目,我们就当一个条目大小为1byte,也需要4GB的大小,更何况每个条目还得存很多数据。所以页表不可能会这么使用。
那么真实的页表到底是什么样子呢?

1.2 页表结构

首先说明几个点:

1️⃣ 进程地址空间的一个地址我们称为虚拟地址,有32个比特位。
2️⃣ 物理内存实际上也划分成了一个一个的数据页。OS为了管理每个数据页,每个数据页都有一个描述的结构体(非常小),存储内存的属性。每个页框大小为4KB
3️⃣ 磁盘上的可执行程序在被编译的时候也被划分成一个一个的4KB大小的数据块,我们把这种4KB的区域称为页帧所以从磁盘加载到内存是以4KB为单位加载的。

虚拟地址的32个比特位并不是以一个整体转化的,而是分成10、10、12三块二进制构成。
而页表也不止一张,分为页目录、和页表。
先拿着虚拟地址的高十位去查页目录,比如如果是0000000001就是第二个位置,映射到指定的页表,再通过中间10个比特位确定物理内存中页框的起始地址,而一个页框的大小是4KB,有2^12字节,刚好对应虚拟地址的低12位比特位,就可以作为页内偏移量找到对应的位置。
在这里插入图片描述
这样我们在使用的时候有可能只使用了几个页表,那么其他的页表就不会加载到内存,只有需要的时候才会创建。由此解决了内存不足的问题。

1.3 线程的理解

首先要知道线程是进程内的一个执行流。

我们知道创建一个进程就会连着创建PCB,虚拟内存、页表。现在我们可以创建一个“进程”(PCB)直接指向虚拟内存,就像下边的绿色的task_struct
在这里插入图片描述
例如代码区有一大段代码,我们现在就可以划分成几个小段代码,分给每个“进程”。这样就实现了资源的分配。

我们可以通过虚拟地址空间和页表对进程进行资源划分,而单个“进程”的执行力度一定要比之前一个进程要细。

1.3.1 如何描述线程

既然有多个线程,那么OS就会采取先描述后组织的方式进行管理。那么怎么描述呢?是创建一个新的结构体来描述吗?

我们知道PCB是用来描述进程的,那么描述线程的结构体我们叫做TCB(线程控制块)
在windows中,就是新创建了一个结构体来描述线程。
而单纯的从线程调度角度,进程和线程有很多地方是重叠的。
所以在linux中,没有创建针对线程的数据结构,而是直接复用PCB,用PCB来表示线程。
而CPU在进行调度的时候不关注到底是进程还是线程,只看task_struct。

总结一下:线程在进程内部(进程的地址空间内)执行,拥有该进程的一部分资源。

1.4 再谈进程

什么叫做进程呢?
在这里插入图片描述
我们把红色框框圈起来的整体叫做进程:
PCB+进程地址空间+页表+加载到物理内存的代码和数据。

从内核角度:进程是承担分配系统资源的基本实体
在linux中:线程是CPU调度的基本单位

而在之前的文章讲过的进程【linux】进程概念详述它讲的是只有一个PCB的进程(只有一个执行流)
今天所讲述的是一个进程内有多个执行流的情况。

从CPU角度:以前调度的就是一个进程,今天就是调度进程中的一个分支
所以现在CPU统一把task_struct看作成轻量级进程

我们知道linux没有正真意义的线程,这相比拥有真正线程的系统有什么优缺点呢?

优点:简单,维护成本大大降低,即可靠又高效。
缺点:linux无法直接提供线程的基本调用接口,只能提供创建轻量级进程的接口

1.5 代码理解

1.5.1 原生库提供线程pthread_create

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.// 链接的时候必须加上-lpthread

RETURN VALUE
On success, pthread_create() returns 0; 
on error, it returns an error number, and the contents of *thread are undefined.

参数说明:
thread:线程id
attr:线程属性,直接设为null
start_routine:函数指针
arg:这个参数会传递进start_routinevoid*参数中。

这里在链接的时候要注意link到系统给的原生线程库-lpthread

在这里插入图片描述
说明一下这个原生线程库:
因为用户只关注线程,但是OS不提供线程的接口,只提供创建轻量级进程的接口。所以在用户和OS之间加了一个用户级线程库。 向上提供各种线程接口,向下把对线程的各种操作转化为对轻量级进程的各种操作。
这个库在任何linux操作系统都默认存在。

// Makefile
mythread:mythread.cc
	g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
	rm -f mythread

// mythread.cc
#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>

using std::cout;
using std::endl;

void* thread_stream(void *str)
{
    while(true)
    {
        cout << "i am new thread" << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one");
    assert(n == 0);
    (void)n;
    // 主线程
    while(true)
    {
        cout << "i am main thread" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
现象:运行了两个执行流,查看只有一个进程,杀死进程两个执行流全部被杀死。
如果想看到这两个轻量级线程:
使用指令:ps -aL
在这里插入图片描述
可以看到两个PID一样,说明属于同一个进程。而这里可以看到LWP不同,这里的LWP就表示轻量级进程ID
细节:主线程的PID和LWP一样。

所以CPU在调度的时候用的就是LWP来作为标识符表示特定的执行流
当只有一个单进程的时候PID和LWP是等价的。

那么这个tid到底是什么呢?
我们可以修改一下代码进行验证:

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");
    assert(n == 0);
    (void)n;
    // 主线程
    while(true)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        cout << "i am main thread " << "tid: " << buf << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
这里只需要知道tid就是一个地址,后面会详细介绍。

1.6 资源共享问题

线程一旦被创建,几乎所有的资源都是被所有线程共享的
比如:

文件描述符表
每种信号处理方式(SIG,IGN,SIG_DFL或者自定义信号处理函数)
当前工作目录
用户id和组id

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include <cstdio>

using std::cout;
using std::endl;

void fun()
{
    cout << "这是一个独立的方法" << endl;
}

void* thread_stream(void *str)
{
    while(true)
    {
        cout << "i am new thread, name: " << (const char*)str;
        fun();
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");
    assert(n == 0);
    (void)n;
    // 主线程
    while(true)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        cout << "i am main thread " << "tid: " << buf;
        fun();
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到这个函数可以被多个线程同时访问。

那么全局变量呢?

int cnt = 0;

void* thread_stream(void *str)
{
    while(true)
    {
        cout << "i am new thread, name: " << (const char*)str << " cnt: " << cnt++ << " &cnt: " << &cnt << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");
    assert(n == 0);
    (void)n;
    // 主线程
    while(true)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        cout << "i am main thread " << "tid: " << buf << " cnt: " << cnt << " &cnt: " << &cnt << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
只要有一个线程中改变了,也会影响另一个进程。
由此可见线程之间通信非常容易。

但是这样又会引发另一个问题。

1.7 资源私有问题

线程也要有自己的私有资源,那么什么资源应该是线程所私有的呢?

1️⃣ PCB的属性(优先级,上下文(线程动态切换),状态……)。
2️⃣ 每一个线程都有自己独立的栈结构保存私有数据。

二、总结

2.1 什么是线程

笼统的讲:线程是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细致和轻量化。

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

2.2 并行与并发

并行:多个执行流在同一刻拿着不同的CPU进行运算。
并发:多个执行流在同一时刻只有一个执行流拥有CPU进行运算。

2.3 线程的优点

1️⃣ 创建线程的代价比创建进程小得多。因为不用创建地址空间、页表、加载代码数据,只用创建一个PCB指向进程地址空间就够了。
2️⃣ 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
进程要切换 页表、PCB、虚拟地址空间……
而线程切换只用切换PCB
那么他们做的工作量到底差距在哪里呢?

在CPU中有一块高速缓存cache,它的效率比寄存器慢,但比内存快。它有局部性原理:当前访问代码附近的代码数据也会被加载进来,有较大的概率被访问到。CPU不会从内存中直接读取数据,而是从cache中获取,没有命中就再从内存中加载数据到cache。而一个已经运行一段时间的进程cache内部会有很多“热点数据”,线程切换的时候并不会更新chche的数据(因为这些热点数据本来就是被线程所共享的),但是进程切换的时候chache内的数据立刻更新。这样chache又得重新缓存数据。

3️⃣ 线程的占有资源比进程小得多。
4️⃣ 能充分利用多处理器的可并行数量。
5️⃣ 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
6️⃣ 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。计算密集型应用最常见的情况有:加密,大数据运算等—主要使用的是CPU资源。
7️⃣ I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

2.4 线程的缺点

1️⃣ 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
2️⃣ 健壮性(鲁棒性)降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
验证:一个线程出现异常会影响其他线程吗?

void* thread_stream(void *str)
{
    while(true)
    {
        cout << "i am new thread, name: " << (const char*)str << endl;
        sleep(1);
        // 一个线程出现异常
        int* p = nullptr;
        *p = 100;
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");
    assert(n == 0);
    (void)n;
    // 主线程
    while(true)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        cout << "i am main thread " << "tid: " << buf << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
原因:线程出现了异常,OS就会发送信号到进程中,这个信号是发送给进程整体的,所以所有线程都会退出。

3️⃣ 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
4️⃣ 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

2.5 线程异常

1️⃣ 单线程如果出现除零或野指针问题导致线程崩溃,进程也会跟着崩溃。
2️⃣ 因为进程具有独立性,导致其他进程最多只是对该进程只读但是不能写。而线程共用的是一个进程的地址空间,线程与线程之间的数据可以互相访问,当一个线程数据出错了,操作系统对该线程发信号,发信号只能发送给该线程对应的进程,进程跟着崩溃了,导致进程内的所有数据被释放,该进程内的其他线程也跟着销毁了(因为线程的数据是进程给的)。所以一个线程崩溃就会导致整个进程崩溃,这也造成了线程的健壮性降低的原因。

2.6 进程与线程间的关系

在这里插入图片描述



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/1295.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV实战——拟合直线

OpenCV实战——拟合直线0. 前言1. 直线拟合2. 完整代码相关链接0. 前言 在某些计算机视觉应用中&#xff0c;不仅要检测图像中的线条&#xff0c;还要准确估计线条的位置和方向。本节将介绍如何找到最适合给定点集的线。 1. 直线拟合 首先要做的是识别图像中可能沿直线对齐的…

7个最受欢迎的Python库,大大提高开发效率

当第三方库可以帮我们完成需求时&#xff0c;就不要重复造轮子了 整理了GitHub上7个最受好评的Python库&#xff0c;将在你的开发之旅中提供帮助 PySnooper 很多时候时间都花在了Debug上&#xff0c;大多数人呢会在出错位置的附近使用print&#xff0c;打印某些变量的值 这个…

设计模式之单例模式~

设计模式包含很多&#xff0c;但与面试相关的设计模式是单例模式&#xff0c;单例模式的写法有好几种&#xff0c;我们主要学习这三种—饿汉式单例&#xff0c;懒汉式单例、登记式单例,这篇文章我们主要学习饿汉式单例 单例模式&#xff1a; 满足要点&#xff1a; 私有构造 …

同一片天空共眠,同一个梦想奋斗《大抠车始歌》(1)

同一片天空共眠&#xff0c;同一个梦想奋斗《大抠车始歌》&#xff08;1&#xff09; English version&#xff1a;Sleeping under the same sky, chasing the same dream - "The Beginning Song of Dakouche" (1) 飞链云 《Sleeping under the same sky, chasing …

Nacos 注册中心 - 健康检查机制源码

目录 1. 健康检查介绍 2. 客户端健康检查 2.1 临时实例的健康检查 2.2 永久实例的健康检查 3. 服务端健康检查 3.1 临时实例的健康检查 3.2 永久实例服务端健康检查 1. 健康检查介绍 当一个服务实例注册到 Nacos 中后&#xff0c;其他服务就可以从 Nacos 中查询出该服务…

jupyter的安装和使用

目录 ❤ Jupyter Notebook是什么&#xff1f; notebook jupyter 简介 notebook jupyter 组成 网页应用 文档 主要特点 ❤ jupyter notebook的安装 notebook jupyter 安装有两种途径 1.通过Anaconda进行安装 2.通过pip进行安装 启动jupyter notebook ❤ jupyter …

10 个超赞的 C 语言开源项目

今天给大家分享10个超赞的C语言开源项目&#xff0c;希望这些内容能对大家有所帮助&#xff01;01.WebbenchWebbench是一个在 Linux 下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL&#xff0c;测试网站在压力下工作的性能。最多可以模拟 3…

【深度强化学习】(8) iPPO 模型解析,附Pytorch完整代码

大家好&#xff0c;今天和各位分享一下多智能体深度强化学习算法 ippo&#xff0c;并基于 gym 环境完成一个小案例。完整代码可以从我的 GitHub 中获得&#xff1a;https://github.com/LiSir-HIT/Reinforcement-Learning/tree/main/Model 1. 算法原理 多智能体的情形相比于单智…

《C++ Primer Plus》(第6版)第12章编程练习

《C Primer Plus》&#xff08;第6版&#xff09;第12章编程练习《C Primer Plus》&#xff08;第6版&#xff09;第12章编程练习1. Cow类2. String类3. Stock类4. Stack类5. 排队时间不超过1分钟6. 再开设一台ATM&#xff0c;重新求解第五题《C Primer Plus》&#xff08;第6版…

JAVA 多线程

目录 P1多线程01&#xff1a;概述 P2多线程02&#xff1a;线程、进程、多线程 P3多线程03&#xff1a;继承Thread类 P4多线程04&#xff1a;网图下载 P5多线程05&#xff1a;实现Runnable接口 P6多线程06&#xff1a;初识并发问题 P7多线程07&#xff1a;龟兔赛跑 P8多…

PyTorch深度学习实战 | 搭建卷积神经网络进行图像分类与图像风格迁移

PyTorch是当前主流深度学习框架之一&#xff0c;其设计追求最少的封装、最直观的设计&#xff0c;其简洁优美的特性使得PyTorch代码更易理解&#xff0c;对新手非常友好。本文为实战篇&#xff0c;介绍搭建卷积神经网络进行图像分类与图像风格迁移。1、实验数据准备本文中准备使…

汇编语言与微机原理(1)基础知识

前言&#xff08;1&#xff09;本人使用的是王爽老师的汇编语言第四版和学校发的微机原理教材配合学习。&#xff08;2&#xff09;推荐视频教程通俗易懂的汇编语言&#xff08;王爽老师的书&#xff09;&#xff1b;贺老师C站账号网址&#xff1b;&#xff08;3&#xff09;文…

10分钟搞定win11安卓子系统

10分钟搞定win11安卓子系统Android子系统的要求一、安装 Windows 虚拟化支持二、Win11 正式版安装安卓子系统方法教程 (离线包安装)三、在Win11 安卓子系统安装 APK 软件包教程Windows 11 WSA 安装 APK 方法&#xff1a;Windows 11上成功运行安卓APP安装国内的 Android 应用商店…

Java序列化与反序列化

优秀博文&#xff1a;IT-BLOG-CN 序列化&#xff1a;把对象转换为字节序列存储于磁盘或者进行网络传输的过程称为对象的序列化。 反序列化&#xff1a;把磁盘或网络节点上的字节序列恢复到对象的过程称为对象的反序列化。 一、序列化对象 【1】必须实现序列化接口Serializabl…

Spring注解驱动开发--AOP底层原理

Spring注解驱动开发–AOP底层原理 21. AOP-AOP功能测试 AOP&#xff1a;【动态代理】 指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式&#xff1b; 1、导入aop模块&#xff1a;Spring AOP&#xff0c;(Spring-aspects) 2、定义一个业务逻辑类(Ma…

Git和Github的基本用法(内含如何下载)

Git和Github的基本用法背景下载安装安装 git for windows使用 Github 创建项目注册账号创建项目下载项目到本地Git工作流程Git 操作的三板斧放入代码三板斧第一招: git add三板斧第二招: git commit三板斧第三招: git push小结背景 git是一个版本控制工具. 主要解决三个问题 代…

[ROC-RK3568-PC] [Firefly-Android] 10min带你了解I2C的使用

&#x1f347; 博主主页&#xff1a; 【Systemcall小酒屋】&#x1f347; 博主追寻&#xff1a;热衷于用简单的案例讲述复杂的技术&#xff0c;“假传万卷书&#xff0c;真传一案例”&#xff0c;这是林群院士说过的一句话&#xff0c;另外“成就是最好的老师”&#xff0c;技术…

蓝桥杯嵌入式第八课--EEPROM读写

前言E2PROM的读写主要是考察IIC的使用&#xff0c;但是在比赛当中I2C的各种驱动文件都是直接给出的&#xff0c;因此我们需要做的工作就是根据EEPROM的读写时序配出读写的函数来。EEPROM硬件连接图我们可以看到IIC的数据线&#xff08;已上拉&#xff09;有两路去处&#xff0c…

C语言详解KMP算法

如果给你一个字符串 和 该字符串的一个子字符串 你能否快速找出该子字符串的所在位置我猜 这里会有一群杠精 说可以找到 真的吗 那下面这个字符串你可以一眼看出来吗你能找出来吗 如果能 算你眼神好 如果不能 那就看看接下来我怎么做你有想到暴力求解法吗&#xff1f;——来自百…

Hadoop运行模块

二、Hadoop运行模式 1&#xff09;Hadoop官方网站&#xff1a;http://hadoop.apache.org 2&#xff09;Hadoop运行模式包括&#xff1a;本地模式、伪分布式模式以及完全分布式模式。 本地模式&#xff1a;单机运行&#xff0c;只是用来演示一下官方案例。生产环境不用。伪分…
最新文章