【Linux】多线程的创建、等待、终止
迪丽瓦拉
2025-06-01 05:49:24
0


目录

一、线程的概念

1、页表详解

1.1页表的举例

1.2页表的真实表示形式

2、进程、线程的区别

2.1进程

2.2线程(Linux中的线程被称为轻量级进程)

3、使用POSIX标准的pthread原生线程库创建“线程”

4、线程中共享和私有数据及读取返回值判定错误

5、线程的优缺点

5.1线程的优点

5.2线程的缺点

6、clone创建两种子进程(了解)

7、用户级线程ID

二、多线程的创建

1、使用Linux原生线程库创建多线程

2、使用C++多线程接口在Linux环境创建多线程

三、线程等待

1、主线程使用pthread_join()等待新线程

2、分离线程

2.1pthread_self()获取线程id

2.2pthread_detach()分离线程

四、线程的终止

1、线程函数return,线程就算终止

2、使用pthread_exit终止线程

3、使用pthread_cancel取消线程

五、对原生线程库进行二次封装

1、Thread.hpp

2、main.cpp


一、线程的概念

1、页表详解

1.1页表的举例

例如线程正在执行一条修改常量区常量的代码,通过页表找到该句代码所对应的执行权限并没有修改权限,内存管理单元MMU将会终止该访问行为,MMU硬件报错,操作系统发现硬件报错,将向进程发送十一号信号SIGSEGV(段错误)终止进程。

1.2页表的真实表示形式

为了解决一一映射页表体积过大问题,采用了类似哈希的结构。

虚拟地址的前10位是页目录,共计2^10个,即1KB大小;中间10位是页表,每一个页目录指向一张页表,每张页表大小1KB,共有1KB张页表,合计大小1MB;后12位代表所属页表指向物理内存的偏移量,加上这个偏移量,即可找到真实的物理地址。

2、进程、线程的区别

2.1进程

进程是承担分配操作系统资源的基本单位。进程=一堆线程PCB+进程地址空间+页表+物理内存的一部分(进程=内核数据结构+进程对应的代码和数据)。

2.2线程(Linux中的线程被称为轻量级进程)

1、线程是CPU调度的基本单位

2、线程(thread)在进程的进程地址空间中运行,拥有进程的一部分资源。进程粒度较粗,线程粒度更细。

3、OS肯定得通过特定的数据结构来管理大量的线程。某些操作系统例如windows,会给线程创建一个个TCB(线程控制块)来管理大量的线程;但是Linux中的线程并不像windows那样,而是直接复用了进程PCB的那套数据结构、管理方法。所以Linux中并没有真正意义上的线程,这些披着进程PCB外壳的“线程”被称为轻量级进程

4、进程用来整体申请资源;线程“伸手”向进程申请资源。Linux没有真正意义上的线程,所以没有办法提供创建线程的系统调用接口,只提供了创建轻量级进程的接口(需要添加对应的原生线程库pthread,它封装了底层的轻量级进程的接口,让程序员在调用该库时感觉像在玩线程)。

3、使用POSIX标准的pthread原生线程库创建“线程”

PTHREAD_CREATE(3)   
#include 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数:1、thread:输出型参数,指向线程标识符的指针,线程创建成功后将通过此指针返回线程标识符。2、attr:线程属性,包括线程的栈大小、调度策略、优先级等信息。如果为NULL,则使用默认属性。3、start_routine:线程启动后要执行的函数指针。4、arg:线程函数的参数,将传递给线程函数的第一个参数。
返回值:pthread_create()成功返回0。失败时返回错误号,*thread中的内容是未定义的。
编译请使用 -pthread 链接。

主线程和新线程两个执行流分别在各自的作用域死循环打印指定内容:

使用Linux线程库记得在编译时链接pthread:

使用ps -aL查看当前操作系统中的线程:

LWP:轻量级进程ID(light weight process)

同一个进程中的线程PID相同,但是LWP不同。主线程的LWD等于PID

4、线程中共享和私有数据及读取返回值判定错误

共享:全局数据、堆空间、加载的的动态库、文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 、当前工作目录、用户id和组id等进程中的大部分资源时共享的;

私有:1、线程PCB属性私有;2、线程有一定的私有上下文结构;3、每个线程都有自己独立的栈结构;

错误:1、传统的一些函数,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。但是pthreads函数出错时不会设置全局变量errno(大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。

2、pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小

5、线程的优缺点

5.1线程的优点

1、创建一个新线程的代价要比创建一个新进程小得多

2、与进程切换相比,线程切换操作系统需要做的工作要少。进程切换:切换页表、进程地址空间、PCB切换、上下文切换;而线程切换仅需PCB切换、上下文切换。

3、线程占用的资源要比进程少很多

4、能充分利用多处理器(多核)的可并行数量

5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

6、计算密集型应用(加密、解密、算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现

7、I/O密集型应用(外设、磁盘、网络等),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

5.2线程的缺点

1、性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

2、健壮性(鲁棒性)降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 一个线程异常退出了,操作系统会向该进程对应的所有PCB发送信号,因为该进程中的所有线程的PID均相同,该信号线程人手一份,全部退出,同样的,进程也因为PID及信号的原因,退出。

3、缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

4、编程难度提高

编写与调试一个多线程程序比单线程程序困难得多。

6、clone创建两种子进程(了解)

CLONE(2)  
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

这个接口是不需要我们使用的,它是fork()和vfork()的底层。区别在于fork创建子进程将不会共享父进程的进程地址空间,而vfork()创建的进程会共享父子进程的进程地址空间(有点轻量级进程那味了)。当然pthread原生线程库的底层,就是使用了clone来为用户创建轻量级进程。

7、用户级线程ID

主线程用进程地址空间中的独立栈结构,新线程使用线程库分配好地址的线程栈。新线程独立的线程栈可以扩容。

线程局部存储:现在有一个全局变量int a=100,存放于已初始化数据段;在a的前面加一个__pthread修饰,每个线程将会各有一份属于自己的变量a,互不干扰,此时a存放于共享区(共享区在pthread库中)。

二、多线程的创建

1、使用Linux原生线程库创建多线程

创建10个新线程,将自定义类对象作为参数传入回调函数中,并将10个对象保存至vector中。

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
class ThreadData
{
public:pthread_t tid;char namebuffer[64];
};
void* start_routine(void* args)//新线程
{ThreadData* td=static_cast (args);//安全的进行强制类型转换int cnt=10;while(cnt--){cout<namebuffer< threads;#define NUM 10//定义创建10个线程//循环创建NUM个线程for(int i=0;inamebuffer,sizeof(td->namebuffer),"%s%d","thread",i);pthread_create(&td->tid,nullptr,start_routine,td);//传递new出来对象的地址,就不会出现缓冲区冲突的问题threads.push_back(td);//pthread_create(&tid,nullptr,start_routine,(void*)"newThread");//pthread_create(&tid,nullptr,start_routine,namebuffer);//传的是缓冲区的地址,所有线程均可访问这段空间//这段堆空间虽然出作用域销毁,但是每个线程的缓冲区都是这个地址//sleep(1);//不睡眠的话,线程缓冲区会被其他进程刷新}while(1){cout<<"mainThread"<

每一个线程都有自己独立的栈结构。

在使用pthread_create函数创建多线程的时候,注意传参不要传入线程共享的参数。

2、使用C++多线程接口在Linux环境创建多线程

void thread_run()
{while(1){cout<<"我是新线程"<

任何语言,在Linux中使用多线程编程,必须使用-pthread进行链接。

C++的thread库,底层有条件编译会判断当前的运行环境,执行适用于Linux或windows的多线程代码。

在Linux环境中,C++的多线程,本质就是对pthread库的封装。

三、线程等待

1、主线程使用pthread_join()等待新线程

和父进程等待子进程一样,进程等待的目的是为了防止子进程变为僵尸进程造成内存泄露同时回收子进程的退出信息;同样的,你可以不关心线程的退出信息,但是一定要回收线程PCB等资源,所以线程等待是必须的。

主线程会在pthread_join()处阻塞式等待新线程的退出。

PTHREAD_JOIN(3)   
#include 
int pthread_join(pthread_t thread, void **retval);
thread:要等哪一个线程
retval:输出型参数,用于获取线程函数返回时的退出结果(回调函数返回值不是void*么,这里用void**接收这个返回值)
返回值:在成功时,pthread_join()返回0; 在错误时,它返回一个错误码。
编译并使用-pthread链接
class ThreadData
{
public:pthread_t tid;char namebuffer[64];
};
void* start_routine(void* args)//新线程
{ThreadData* td=static_cast (args);//安全的进行强制类型转换int cnt=10;while(cnt--){cout<namebuffer< threads;#define NUM 10//定义创建10个线程//循环创建NUM个线程for(int i=0;inamebuffer,sizeof(td->namebuffer),"%s%d","thread",i);pthread_create(&td->tid,nullptr,start_routine,td);//传递new出来对象的地址,就不会出现缓冲区冲突的问题threads.push_back(td);}//进程对线程进行等待for(auto& iter : threads){void* ret=nullptr;int n=pthread_join(iter->tid,&ret);//用ret取到返回值void*assert(0==n);cout<<"退出成功"<<(long long)ret<

线程会将void*的退出信息传入pthread库,通过pthread_join()函数中的参数void **retval接收这个退出信息。

我们知道,父进程是可以通过设置对SIGCHLD信号忽略的做法,无视子进程的退出信号,转而让操作系统去回收子进程的资源。那如果主线程对新线程的退出信息根本不关心,能否像进程那样让操作系统去回收新线程的资源呢?可以通过分离线程的方法达到该目的。

2、分离线程

2.1pthread_self()获取线程id

PTHREAD_SELF(3)     
#include 
pthread_t pthread_self(void);
返回值:此函数始终成功,返回调用线程的ID。
编译并使用-pthread链接

2.2pthread_detach()分离线程

PTHREAD_DETACH(3) 
#include 
int pthread_detach(pthread_t thread);
thread:线程ID
返回值:在成功时,pthread_detach()返回0; 在错误时,它返回一个错误码。
编译并使用-pthread链接

错误示范:

把pthread_detach放在新线程的执行函数里,有可能发生主线程已经在join处开始等待了,新线程才走到执行分离的代码,等新线程执行完回调函数内的代码时,主线程自然join等待成功了。这是错误写法。

正确写法:创建线程成功时,由主线程进行分离

void* start_routine(void* args)
{string threadname=static_cast(args);sleep(5);return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");pthread_detach(tid);//创建线程成功时,由主线程进行分离// int n=pthread_join(tid,nullptr);// cout<

四、线程的终止

切忌在新线程中使用exit(0)等终止进程的函数来终止线程,因为整个进程会被干掉。

1、线程函数return,线程就算终止

void* start_routine(void* args)//新线程
{ThreadData* td=static_cast (args);//安全的进行强制类型转换int cnt=10;while(cnt--){cout<namebuffer<

2、使用pthread_exit终止线程

void* start_routine(void* args)//新线程
{ThreadData* td=static_cast (args);//安全的进行强制类型转换cout<namebuffer<

使用pthread_exit的主要场景是在线程中发生了错误或者需要提前退出时使用。比如,当一个线程检测到某个条件不满足时,可以调用pthread_exit函数来立即退出线程。 需要注意的是,如果在线程中使用pthread_exit提前退出线程,需要确保已经释放了该线程分配的资源,否则可能会导致资源泄漏。

3、使用pthread_cancel取消线程

PTHREAD_CANCEL(3)     
#include 
int pthread_cancel(pthread_t thread);
thread:要取消哪一个线程
返回值:在成功时,pthread _ Cancel ()返回0; 在出错时,它返回一个非零的错误码。
编译并使用-pthread链接
for(auto& iter : threads)
{pthread_cancel(iter->tid);
}

线程如果被取消,那么这个线程的退出码是-1(宏PTHREAD_CANCELED)。

五、对原生线程库进行二次封装

1、Thread.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#define NUM 1024
class Thread;//声明一下
class Context//线程上下文类
{
public:Context():_this(nullptr),_args(nullptr){}~Context(){}
public:Thread* _this;void* _args;
};
class Thread
{
public:typedef std::function func_t;//定义一个函数对象类型,它的返回值和参数都是void*//构造函数Thread(func_t func,void* args,int number)//number是线程名字,后续int转string:_func(func),_args(args){char buffer[NUM];snprintf(buffer,sizeof(buffer),"thread-%d",number);_name=buffer;//线程启动Context* ctx=new Context();ctx->_this=this;ctx->_args=_args;int n=pthread_create(&_tid,nullptr,start_routine,ctx);assert(n==0);(void)n;}void* run(void* args){return _func(args);}//因为形参有个this指针,所以弄成staticstatic void* start_routine(void* args)//这里的args就是 start()的ctx{Context* ctx=static_cast(args);void* ret=ctx->_this->run(ctx->_args);delete ctx;return ret;} void join(){int n=pthread_join(_tid,nullptr);assert(n==0);(void)n;}~Thread(){}private:std::string _name;//线程的名字func_t _func;//线程的回调函数void* _args;//喂给线程的参数,不过是用Context类封装一下喂给线程pthread_t _tid;//线程ID
};
//异常和if。意料之外
//assert。意料之中。99%概率为真。

2、main.cpp

#include 
#include 
#include 
#include "Thread.hpp"
using namespace std;   
void* thread_run(void* args)
{string work_type=static_cast(args);while(1){cout<<"新线程"< thread1(new Thread(thread_run,(void*)"thred1",1));unique_ptr thread2(new Thread(thread_run,(void*)"thred2",2));unique_ptr thread3(new Thread(thread_run,(void*)"thred3",3));thread1->join();thread2->join();thread3->join();return 0; 
}

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...