【Linux】线程概念与控制

一. 线程的概念

1.什么是线程

线程是进程内部的一个执行流,是进程调度的基本单位。它具有轻量的特点,它的创建和销毁所消耗的资源更少,线程间切换比进程间切换消耗的资源更少;它与进程共享一张虚拟地址空间表,通过进程来给线程执行流分配资源;同时,每个线程都是独立执行的,拥有自己的程序计数器和上下文切换。

简单来说在Linux下,一个进程由多个 task_struct ,一张虚拟地址空间表和页表构成。而线程就是一个 task_struct ,进程内部的一个执行流,所有的线程都指向同一张虚拟地址空间表,让进程共同管理。这样我们就可以对所有线程的资源进行划分,划分为堆区栈区共享区等等。通过一张页表映射到物理内存当中。

总的来说,线程就是一个 task_struct ,通过共同指向同一张虚拟地址空间的方式实现了共同管理,降低了创建调度销毁成本。

2.深刻理解虚拟地址空间

在虚拟地址空间中,页表用于映射虚拟地址空间到实际的物理内存。我们在管理虚拟地址空间的时候,它的地址是连续的,而物理地址空间则是可以分散碎片的。在虚拟地址空间中,我们存储同一个资源的时候地址空间需要连续,但在物理地址当中,我们会将同类型的资源尽可能放到一处(这样可以节省空间),无论是哪个线程都可以将数据进行整合。

有了虚拟地址空间和物理地址空间,那么我们如何将他们连接起来呢?再加一层页表就好。

在 Linux 当中,页表是由,三级页表组成一级页表是页表目录,其中存储着各个页表的地址;二级目录是各个页表,页表指向各个页帧的地址(4 KB);三级页表就是页帧,每个页帧由 4 KB 构成。在虚拟地址空间中,每个数据在虚拟地址空间下都有一个 32 字节的地址,这 32 个字节需要分为 10 + 10 + 12 来进行阅读,首先定位到页表目录当中,前10个字节,用于在页表目录当中找到对应的页表;中间的10个字节用于在当前页表当中找到对应的页帧;最后的12个字节用于对页帧的起始位置的偏移量,这样我们就能通过虚拟地址找到相对于的物理地址

下面是一个简化图

有了页帧,该如何管理呢? 先描述再组织,操作系统引入了 struct page 结构。对于每个页帧,都有一个 struct page 对它进行相应的管理。

下面我来介绍一下 struct page 的结构构成。

该结构主要用于管理记录,跟踪页帧的使用状态,页针的归属,管理页帧的映射关系,回收页帧等等。

首先是状态标识(flags),用于记录页帧的基本状态,是被锁定被修改还是内核保留;引用计数(_refcount)记录该页帧被引用的次数;映射关系(mapping + index)mapping 指向该文件的存储页,index 用于指向在该页下的偏移量。


总结:

1. 虚拟地址和物理地址管理,通过页表进行映射,使得其完成了解耦的操作。

2. 页表按需创建和分页机制有效的节省了空间消耗。

3.线程的优缺点

(1)优点

线程的创建相比于进程的创建代价要小很多且占用资源少,线程只需要创建 task_struct 挂接到虚拟地址空间上即可,而进程的创建就要涉及虚拟地址空间页表等等资源;线程切换比进程切换效率高,如果要进行进程间的切换,就需要连同虚拟地址空间等进行统一切换,而线程只需要切换 task_struct 和上下文资源即可;线程可以利用多处理器进行并发运行,提高 IO 效率和计算效率;

(2)缺点

线程共享进程的地址空间,因此可以访问到当前的共享资源,这就导致,若缺乏同步机制,线程会引发数据竞争,导致程序异常;当线程过多时,容易导致资源限制,首先每个线程都有自己的独立线程栈在内存当中,大小为 8 MB ,若线程过多就容易导致内存空间耗尽。其次,若线程过多,CPU的调度开销也对应的增加,CPU 将时间花在了不断调度线程中,导致 CPU 利用率下降。最后,每个线程创建都会在内核当中创建一个 TCB 资源,这也就导致了高频创建销毁会给内核增加负担。进程拥有较高的独立性,即使程序出错进程崩溃,这也不会影响其他的进程运行,但如果线程崩溃,可能会导致整个进程都退出。

二. 线程的控制

1.线程创建

pthread_create:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);

参数说明:

thread:获取创建成功的线程 ID ,该参数是一个输出型参数。

attr:用于设置进程属性,传入NULL 表示使用默认值。

start_routine:返回值和参数均为 void* 的函数指针。该参数表示线程例程,即后续线程需要执行的函数。

arg:传给线程实例的参数。

返回值:

成功返回0,失败返回错误码。

下面我们来看一个示例,让一个主线程创建一个新线程

当一个程序启动时,就有一个进程被操作系统创建,于此同时一个线程也立刻运行,这个线程就是主线程。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *startRoutine(void* args)
{while(true){cout<<"线程正在运行"<<endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread-1");cout<<"new thread id:"<<tid<<endl;while(true){cout<<"main pthread 正在运行"<<endl;sleep(1);}return 0;
}

运行结果:


当我们想获取线程 id 时,可以使用 pthread_self 函数,我们来看下面的代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;static void Print(const char* name,pthread_t tid)
{cout<<name<<" 正在运行"<<tid<<endl;
}void *routine(void* argv)
{const char* name = static_cast<const char*>(argv);while(true){Print(name,pthread_self());sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid,nullptr,routine,(void*)"thread");while(true){cout<<"main thread run"<<endl;sleep(1);}return 0;
}

下面是运行结果:

在线程运行中,调用了 pthread_self 函数将当前线程的tid传给了函数进行调用。


2.线程终止

终止一个线程有三种方法:

1.从线程函数 return

2.在线程中调用

3.在线程中调用 pthread_exit 终止其它进程中的另一个线程

方法一:(从线程return)

方法较简单不详细讲解

方法二:(pthread_exit

pthread_exit 的功能就是终止线程

#include <pthread.h>
void pthread_exit(void* retval);

参数说明:

retval:线程退出码

注意:

pthread_exit return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,若是在线程内部创建的指针返回会导致访问结果不可控。因为随着 pthread_exit 线程栈上存储的数据也会被销毁

下面看一下正确使用:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;static void printTid(const char* name,const pthread_t &tid)
{cout<<name<<" 正在运行 "<<tid<<"  "<<endl;
}void* Routine(void* argv)
{const char* name = static_cast<const char*>(argv);int cnt = 5;while(true){printTid(name,pthread_self());if(!(cnt--)){break;}sleep(1);}cout<<"线程退出"<<endl;pthread_exit((void*)11111);
}int main()
{pthread_t tid;int n = pthread_create(&tid,nullptr,Routine,(void*)"thread");void* ret = nullptr;pthread_join(tid,&ret);cout<<"main pthread success   "<<(long long)ret<<endl;sleep(5);while(true){printTid("main othread",pthread_self());sleep(2);}return 0;
}

运行结果:


3.线程等待

pthread_join:

类比于进程等待,线程创建也是需要被等待的,如果一个新线程被创建出来,主线程不进行等待,那么这个新线程的资源就无法被回收,就会导致资源泄露。在线程中等待的函数叫 pthread_join 

#include <pthread.h>
int pthread_join(pthread_t thread,void **retval);

参数说明:

thread:被等待的线程tid

retval:线程退出时的信息码

返回值:

成功返回0,失败返回信息码

下面是代码样例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void *thread_func(void *arg) 
{printf("子线程任务完成\n");pthread_exit((void*)100);  sleep(200);
}int main() 
{pthread_t tid;void *ret;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, &ret);  printf("子线程退出状态:%ld\n", (long)ret);return 0;
}

运行结果:

主线程进行阻塞,等待子线程完成任务后,主线程才会继续运行

4.线程分离

pthread_detach:

线程分离与线程等待是一对互斥关系,当我们主线程不需要关心子线程的返回值时,我们可以将子线程进行分离(也可以是子线程自行分离),分离后的线程会继续执行自己的内容。一个线程被分离了,这个进程依旧需要管理这个线程的资源,若被分离的线程出现故障也有可能会影响其他的线程或者当前进程。分离的线程可以减轻 join 的负担,意味着主线程不需要再关注子线程了,而子线程执行完毕后也会自行释放资源。

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数说明:

thread:被分离的线程 ID 

返回值:

成功返回0,失败返回错误码


5.POSIX线程库

在Linux当中,站在内核角度实际上并没有关于线程相关的接口,但是用户希望创建线程时可以调用接口,这样可以使编码更加便捷。于是,便有了第三方的线程库,基于这个第三方库,它为用户提供了线程相关的接口,构成了线程有关的完整系列。

这些接口大多数都是以 pthread_  打头,在使用前需要包含头文件  <pthread.h> ,链接库时需要包含  -lpthread 选项。

6.线程栈和 pthread_t

线程是一个独立的执行流,在运行的过程中也会产生自己的数据,所以线程拥有自己的独立的栈,线程栈会随着线程的销毁被回收。

在 Linux 中,基于线程的接口都是通过外部库封装后进行调用的,pthread_t 是线程的身份证,用于识别和操作线程。在外部库中,pthread_t 是由 thread_info 结构体进行管理的。

struct thread_info
{pthread_t tid;void *stack;
}

与其一同管理的便是线程栈。每当用户创建一个线程时,就会在动态库中创建一个线程控制块 thread_info ,给用户返回一个 pthread_t ,也就是该结构体的起始虚拟地址。

主线程中的栈区使用的是地址空间中的栈区,而创建的子线程用的是库中提供的栈结构。

7.线程的局部存储

在线程中,全局变量是共享的,所有的线程可以共用一份全局变量,如果想让全局变量私有那么可以进行线程变量的局部存储

下面我们来验证一下,线程可以共用同一份全局变量

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;int global_val = 100;void *Routine(void *argv)
{const char* name = static_cast<const char*>(argv);while(true){cout<<"thread: "<<name<<"  global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl;sleep(1);}
}int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1,nullptr,Routine,(void*)"thread1");pthread_create(&tid2,nullptr,Routine,(void*)"thread2");pthread_create(&tid3,nullptr,Routine,(void*)"thread3");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);return 0;
}

运行结果:

我们发现,全局变量在主线程和子线程下该变量的地址都是一致的,它们所用的是同一个变量

若我们希望在每一个子线程下都创建一份变量我们可以这样操作

样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;__thread int global_val = 100;void *Routine(void *argv)
{const char* name = static_cast<const char*>(argv);while(true){cout<<"thread: "<<name<<"  global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl;sleep(1);}
}int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1,nullptr,Routine,(void*)"thread1");pthread_create(&tid2,nullptr,Routine,(void*)"thread2");pthread_create(&tid3,nullptr,Routine,(void*)"thread3");pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);pthread_join(tid3,nullptr);return 0;
}

运行结果:

我们只要在全局变量前加上  __thread  ,此时所有的线程都在自己的栈上拿到了一份数据,我们可以观察到,此时全局变量打印出的地址是不同的,且变量是肚子增加的。


三. 线程的封装

线程封装

我们简单的对线程进行封装,使其能进行创建分离等待终止等功能

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <functional>
#include <pthread.h>
using namespace std;static uint32_t number = 1;template <typename T>
class Thread
{using func_t = function<void(T)>;private:void EnableDetach(){std::cout << "线程被分离了" << std::endl;_isdetach = true;}void EnableRunning(){_isrunning = true;}static void *Routine(void *argv){Thread<T> *self = static_cast<Thread<T> *>(argv);self->EnableRunning();if (self->_isdetach)self->Detach();self->_func(self->_data); // 回调处理return nullptr;}public:Thread(func_t func, T Data): _tid(0), _isrunning(false), _isdetach(false), _Data(Data), _func(func){_name = "Thread - " + to_string(_number++);}void Detach(){if (_isdetach)return;int n = pthread_detach(_tid);if (n != 0){cerr << "fail to detach" << strerror(n) << endl;}else{cout << "success to detach" << endl;_isdetach = true;}}void Join(){if (_isdetach){cout << "线程已经分离,无法进行等待" << endl;return;}int n = pthread_join(_tid, &res);if (n != 0){cerr << "fail to join" << strerror(n) << endl;}else{cout << "success to join" << endl;}}bool Start(){if (_isrunning)return false;int n = pthread_create(&tid, nullptr, Routine, this);if (n != 0){cerr << "fail to create pthread" << strerror(n) << endl;return false;}else{cout << "success to create pthread" << strerror(n) << endl;return true;}}bool Stop(){if (!_isrunning)return false;int n = pthread_cancel(tid);if (n != 0){cerr << "fail to Stop" << strerror(n) << endl;return false;}else{cout << "success to Stop" << endl;_isrunning = false;return true;}}~Thread(){}private:string _name;pthread_t _tid;bool _isrunning;bool _isdetach;T _Data;void *res;func_t _func;
};

感谢各位观看,望多多支持!!!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

双轴倾角传感器厂家与物联网角度传感器应用全解析

本文主要探讨双轴倾角传感器厂家的核心技术优势&#xff0c;以及物联网角度传感器在智能监测中的创新应用。同时&#xff0c;也详细介绍了水平监测传感器厂家的解决方案特点&#xff0c;并分析了专业进口倾角传感器代理所提供的原厂品质保障与本地化服务支持。以深圳瑞惯科技有…

容器-资源隔离机制

一. 引言&#xff1a; 大家都知道&#xff0c;在一台机器上&#xff0c;可以运行任意(根据系统资源)个容器实例。且各容器间是相互独立&#xff0c;不做任何关联的。那么&#xff0c;docker是通过什么方式来实现容器隔离的呢&#xff1f; 接下来我们了解下。 二. 关于容器隔离…

Agentic RL Survey: 从被动生成到自主决策

Agentic RL Survey: 从被动生成到自主决策 本文将系统解读《The Landscape of Agentic Reinforcement Learning for LLMs: A Survey》这篇综述。该综述首次将智能体强化学习&#xff08;Agentic RL&#xff09;与传统LLM-RL范式正式区分&#xff0c;通过MDP/POMDP理论框架梳理…

彻底禁用 CentOS 7.9 中 vi/vim 的滴滴声

在 VMware 虚拟机中安装的 CentOS 7.9 系统&#xff0c;即使通过修改 /etc/inputrc 禁用了终端铃声&#xff08;set bell-style none&#xff09;&#xff0c;vi 或 vim 编辑时仍可能发出滴滴声。这是因为 vi/vim 有自己独立的铃声控制机制。以下是解决方法&#xff1a;方法 1&…

基于A2A和ADK的内容规划代理

项目概述 Content Planner Agent 是一个基于 Google Agent Development Kit (ADK) 和 Python A2A SDK 构建的智能内容规划代理。该代理能够根据高层次的内容描述&#xff0c;创建详细的内容大纲。 什么是A2A Protocol A2A Protocol&#xff08;Agent2Agent 协议&#xff09;…

Linux-条件变量

文章目录条件变量概述条件变量的优缺点条件变量相关函数pthread_cond_init函数pthread_cond_destroy函数pthread_cond_wait函数pthread_cond_signal函数测试生产者和消费者模型条件变量 概述 与互斥锁不同&#xff0c;条件变量是用来等待而不是用来上锁的&#xff0c;条件变量…

[硬件电路-166]:Multisim - SPICE与Verilog语言的区别

SPICE与Verilog语言在电子设计领域中扮演不同角色&#xff0c;SPICE是电路仿真语言&#xff0c;用于精确模拟电路行为&#xff1b;Verilog是硬件描述语言&#xff0c;用于描述数字电路的结构和行为。以下是两者的详细区别&#xff1a;一、核心定位与用途SPICE&#xff1a;电路仿…

玩转Docker | 使用Docker部署Umbrel操作系统

玩转Docker | 使用Docker部署Umbrel操作系统 前言 一、 Umbrel 介绍 Umbrel简介 Umbrel主要特点 二、系统要求 环境要求 环境检查 Docker版本检查 检查操作系统版本 三、部署Umbrel服务 下载Umbrel镜像 编辑部署文件 创建容器 检查容器状态 检查服务端口 安全设置 四、访问Umbr…

Flink Task线程处理模型:Mailbox

Task的线程 和 MailboxProcessor 的绑定executingThread 是 Task 类&#xff08;StreamTask 的父类&#xff09;在构造时创建的物理线程。MailboxProcessor 是 StreamTask 用来处理异步事件和驱动其主要处理逻辑&#xff08;processInput&#xff09;的核心组件。它们之间的绑定…

OpenCV 银行卡号识别

目录 一、项目原理与核心技术 二、环境准备与工具包导入 1. 环境依赖 2. 工具包导入 三、自定义工具类 myutils.py 实现 四、主程序核心流程&#xff08;银行卡识别.py&#xff09; 1. 命令行参数设置 2. 银行卡类型映射 3. 辅助函数&#xff1a;图像展示 五、步骤 1…

计算机二级Python

一.静态语言和脚本语言高级语言根据计算机执行机制的不同分为两类&#xff1a;静态语言和脚本语言静态语言的核心特征&#xff1a;变量的类型在编译时&#xff08;写代码时&#xff09;就必须确定并固定下来&#xff0c;即在使用一个变量前必须显式地声明它地类型一旦声明&…

Mybatis Log Plugin打印日志,会导致CPU升高卡死

原因 大量日志输出:MyBatis Log Plugin 会打印大量的 SQL 日志,包括 SQL 语句及其参数。如果项目中 SQL 查询频繁且复杂,日志量会非常大,导致 CPU 使用率升高,甚至卡死。 日志级别设置不当:如果将日志级别设置为 DEBUG 或 TRACE,MyBatis 会输出非常详细的日志信息,这会…

鸿蒙:深色模式适配和浅色模式的切换

前言&#xff1a; 有些时候我们需要对应用进行深色模式的适配处理&#xff0c;并且在不需要的时候切换到浅色状态&#xff0c;下面和大家一起照着官方文档来学习。 下面是官方文档的链接&#xff1a; https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-dark-…

Coze源码分析-资源库-删除插件-后端源码-数据访问和基础设施层

5. 数据访问层 5.1 仓储接口定义 插件仓储接口 文件位置&#xff1a;backend/domain/plugin/repository/plugin.go type PluginRepository interface {// DeleteDraftPlugin 删除插件草稿DeleteDraftPlugin(ctx context.Context, pluginID int64) error// DeleteAPPAllPlugins …

案例一: 对基础选择器的使用【网页盒子】

【1】样例&#xff1a;首先&#xff0c;观察到&#xff0c;几个元素竖着排列的&#xff0c;所以使用块级元素&#xff0c;而不是行内元素。【2】代码演示<head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width,…

爬虫项目优化:如何用 Redis 实现 “断点续爬”?避免重复采集电商数据

在电商数据采集场景中&#xff0c;爬虫常因网络波动、服务器重启、IP 封禁等问题中断。若缺乏断点续爬机制&#xff0c;重启后需从头开始&#xff0c;不仅浪费带宽与时间&#xff0c;还可能因重复采集导致数据冗余。Redis 凭借其高性能、原子操作、多样数据结构的特性&#xff…

决策树概念与原理

决策树简介决策树是一种树形结构树中每个内部节点表示一个特征上的判断&#xff0c;每个分支代表一个判断结果的输出&#xff0c;每个叶子节点代表一种分类结果(仅举例无其他意义或隐喻)就像一个女孩去相亲&#xff0c;那么首先询问是否大于30&#xff0c;大于则不见&#xff0…

SQL面试题及详细答案150道(116-135) --- 高级查询与函数篇

《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,MySQL,Linux… 。 前后端面试题-专栏总目录 文章目录 一、本文面试题目录 116. 如何使用CASE语句实…

VeRL:强化学习与大模型训练的高效融合框架

本文由「大千AI助手」原创发布&#xff0c;专注用真话讲AI&#xff0c;回归技术本质。拒绝神话或妖魔化。搜索「大千AI助手」关注我&#xff0c;一起撕掉过度包装&#xff0c;学习真实的AI技术&#xff01; 1 概述&#xff1a;VeRL的起源与核心价值 VeRL&#xff08;Versatile…

2. 计算机系统基础知识

1 计算机系统概述 计算机系统 (Computer System) 是指用于数据管理的计算机硬件、软件及网络组成的系统。 计算机系统可划分为硬件(子系统)和软件(子系统)两部分。硬件由机械、电子元器件、磁介质和光介质等物理实体构成&#xff0c;例如处理器(含运算单元和控制单元)、存储器、…