【Linux】线程概念|线程理解|线程控制

文章目录

  • 线程概念
  • Linux中线程是否存在的讨论
  • 线程创建和线程控制
    • 线程的终止和等待(三种终止方式 + pthread_join()的void**retval)

线程概念

线程就是进程内部的一个执行流,线程在进程内运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。这句话一说可能老铁们直接蒙蔽,线程就线程嘛,怎么还在进程里面运行呢?还在地址空间内运行?而且拥有进程的一部分资源,这都是什么鬼?
如何看待线程在地址空间内运行呢?实际进程就像一个封闭的屋子,线程就是在屋子里面的人,而地址空间就是一个个的窗户,屋子外面就是进程对应的代码和数据,一个屋子里面当然可以有多个人,而且每个人都可以挑选一个窗户看看外面的世界。

在上面的例子中,每个人挑选一个窗户实际就是将进程的资源分配给进程内部的多个执行流,以前fork创建子进程的时候,不就是将父进程的一部分代码块儿交给子进程运行吗?子进程不就是一个执行流吗?
而今天我们所谈到的线程道理也是类似,我们可以将进程的资源划分给不同的线程,让线程来执行某些代码块儿,而线程就是进程内部的一个执行流。那么此时我们就可以通过地址空间+页表的方式将进程的部分资源划分给每一个线程,那么线程的执行粒度一定比之前的进程更细!

Linux中线程是否存在的讨论

我们在思考一下,如果Linux在内核中真的创建出了我们上面所谈论到的线程,那么Linux就一定要管理内核中的这些线程,既然是管理,那就需要先描述,再组织创建出真正的 TCB(Thread Create Block)结构体来描述线程,线程被创建的目的不就是被执行,被CPU调度吗?既然所有的线程都要被调度,那每个线程都应该有自己独立的thread_id,独立的上下文,状态,优先级,独立的栈(线程执行进程中的某一个代码块儿)等等,那么大家不觉得熟悉吗?单纯从CPU调度的角度来看,线程和进程有太多重叠的地方了!
所以Linux中就没有创建什么线程TCB结构体,直接复用进程的PCB当作线程的描述结构体,用PCB来当作Linux系统内部的"线程"。这么做的好处是什么呢?如果要创建真正的线程结构体,那就需要对其进行维护,需要和进程构建好关系,每个线程还需要和地址空间进行关联,CPU调度进程和调度线程还不一样,操作系统要对内核中大量的进程和线程做管理,这样维护的成本太高了!不利于系统的稳定性和健壮性,所以直接复用PCB是一个很好的选择,维护起来的成本很低,因为直接复用原来的数据结构就可以实现线程。所以这也是Linux系统既稳定又高效,成为世界上各大互联网公司服务器系统选择的原因。(而windows系统内是真正有对应的TCB结构体的,他确实创建出了真正的线程,所以维护起来的成本就会很高,这也是windows用的用的就卡起来,或者蓝屏的原因,因为不好维护啊,实现的结构太复杂!代码健壮性不高)

在知道linux的线程实现方案之后,我们又该如何理解线程这个概念呢?现在PCB都已经不表示进程了,而是代表线程。以前我们所学的进程概念是:进程的内核数据结构+进程对应的代码和数据,但今天站在内核视角来看,进程的概念实际可以被重构为:承担分配系统资源的基本实体!进程分配了哪些系统资源呢?PCB+虚存+页表+物存。所以进程到底是什么呢?
那在linux中什么是线程呢?线程是CPU调度的基本单位,也就是struct task_struct{},PCB就是线程,为进程中的执行流!
那我们以前学习的进程概念是否和今天学习的进程概念冲突了呢?当然没有,以前的进程也是承担分配系统资源的基本实体,只不过原来的进程内部只有一个PCB,也就是只有一个执行流,而今天我们所学的进程内部是有多个执行流,多个PCB!

在这里插入图片描述

所以: Linux内核中有没有真正意义上的线程, Linux用进程的PCB来模拟线程,是完全属于自己实现的一套方案!
站在CPU的角度来看,每一个PCB,都可以称之为轻量级进程,因为它只需要PCB即可,而进程承担分配的资源更多,量级更重!
Linux线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体!
进程用来整体向操作系统申请资源,线程负责向进程伸手要资源。如果线程向操作系统申请资源,实质上也是进程在向操作系统要资源,因为线程在进程内部运行,是进程内部的一部分!
Linux内核中虽然没有真正意义上的线程,但虽无进程之名,却有进程之实!
程序员只认线程,但Linux没有线程只有轻量级进程,所以Linux无法直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口!


线程创建和线程控制

在这里插入图片描述

#include <iostream>
#include <string>
#include<unistd.h>using namespace std;void *start_routine(void *arg)
{string name = static_cast<const char *>(arg);while (true){cout << "new thread: " << name << endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void *)"thread-1");while (true){cout << "main thread" << endl;sleep(1);}return 0;
}

在这里插入图片描述
创建一个线程比较简单没什么含金量,所以在线程控制这里选择创建一批线程,来看看多个线程下的进程运行情况。
在线程的错误检查这里,并不会设置全部变量errno,道理也很简单,线程出错了,那其实就是进程出错了,错误码这件事不应该是我线程来搞,这是你进程的事情和我线程有什么关系?所以线程也没有理由去设置全局变量errno,他的返回值只表示成功或错误,具体的返回状态,其实是要通过pthread_join来获取的!
在这里插入图片描述

创建一批线程也并不困难,我们可以搞一个vector存放创建出来的每个线程的tid,但从打印出来的新线程的编号可以看出来,打印的非常乱,有的编号还没有显示,这是为什么呢?(我们主观认为应该是打印出来0-9编号的线程啊,这怎么打印的这么乱呢?)
其实这里就涉及到线程调度的话题了,创建出来的多个新线程以及主线程谁先运行,这是不确定的,这完全取决于调度器,我们事先无法预知哪个线程先运行,所以就有可能出现,新线程一直没有被调度,主线程一直被调度的情况,也有可能主线程的for循环执行到i等于6或9或8的时候,新线程又被调度起来了,此时新线程内部就会打印出创建成功的语句。所以打印的结果很乱,这也非常正常,因为哪个线程先被调度是不确定的!在这里插入图片描述

线程的终止和等待(三种终止方式 + pthread_join()的void**retval)

再谈完线程的创建之后,那什么时候线程终止呢?所以接下来我们要谈论的就是线程终止的话题,线程终止总共有三种方式,分别为return,pthread_exit,pthread_cancel
我们知道线程在创建的时候会执行对应的start_routine函数指针指向的方法,所以最正常的线程终止方式就是等待线程执行完对应的方法之后,线程自动就会退出,如果你想要提前终止线程,可以通过最常见的return的方式来实现,线程函数的返回值为void*,一般情况下,如果不关心线程退出的情况,直接return nullptr即可。
和进程终止类似的是,除return这种方式外,原生线程库还提供了pthread_exit接口来终止线程,接口的使用方式也非常简单,只要传递一个指针即可,同样如果你不关心线程的退出结果,那么也只需要传递nullptr即可。

#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 10 
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t  _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr;  //线程终止pthread_exit(nullptr);
}int main()
{//创建一批线程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//创建一个线程数据对象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);sleep(1);}return 0;
}

在这里插入图片描述

谈完上面两种线程终止的话题后,第三种终止方式我们先等会儿再说,与进程类似,进程退出之后要被等待,也就是回收进程的资源,否则会出现僵尸进程,僵尸的这种状态可以通过ps指令+axj选项看到,同时会产生内存泄露的问题。
线程终止同样也需要被等待,但线程这里没有僵尸线程这样的概念,如果不等待线程同样也会造成资源泄露,也就是PCB资源未被回收,线程退出的状态我们是无法看到的,我们只能看到进程的Z状态。
原生线程库给我们提供了对应的等待线程的接口,其中join的第二个参数是一个输出型参数,在join的内部会拿到线程函数的返回值,然后将返回值的内容写到这个输出型参数指向的变量里面,也就是写到我们用户定义的ret指针变量里,通过这样的方式来拿到线程函数的返回值。
通过bash的打印结果就可以看到,每个线程都正常的等待成功了。

#include <iostream>
#include <string>
#include<unistd.h>
#include<vector>
#include <stdio.h>
#include<functional>
#include <time.h>
#include <pthread.h>#define NUM 5 
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f):_name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t  _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void* start_routine(void* arg)
{ThreadData* td = static_cast<ThreadData*> (arg);cout << "I am a new thread, my name is : "<< td->_name << " creatname is: " << td->_createtime << endl; td->_func_t();//return nullptr;  //线程终止//pthread_exit(nullptr);return (void*)110;
}int main()
{vector<pthread_t> tids;//保存线程的tid//创建一批线程for(int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread" , i+1);//创建一个线程数据对象string tdname = threadname;ThreadData* td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);sleep(1);}void* retval = nullptr;for(int i = 0; i < NUM; i++){//线程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;sleep(1);}return 0;
}

在这里插入图片描述

在了解join拿到线程函数的返回值之后,我们再来谈最后一个线程终止的方式pthread_cancel,叫做线程取消。首先线程要被取消,前提一定得是这个线程是跑起来的,跑起来的过程中,我们可以选择取消这个线程,换个说法就是中断这个线程的运行。
如果新线程是被别的线程取消的话,则新线程的返回值是一个宏PTHREAD_CANCELED,这个宏其实就是把-1强转成指针类型了,所以如果我们join被取消的线程,那join到的返回值就应该是-1,如果线程是正常运行结束退出的话,默认的返回值是0.
我们让创建出来的每个新线程跑10s,然后在第5s的时候,主线程取消前5个线程,那么这5个线程就会被中断,主线程阻塞式的join就会提前等待到这5个被取消的线程,并打印出线程函数的返回值,发现结果就是-1,再经过5s之后,其余的5个线程会正常的退出,主线程的join会相应的等待到这5个线程,并打印出默认为0的退出结果。

#include <iostream>
#include <string>
#include <unistd.h>
#include <vector>
#include <stdio.h>
#include <functional>
#include <time.h>
#include <pthread.h>#define NUM 10
using namespace std;
// using func_t = function<void()>;
typedef function<void()> func_t;class ThreadData
{
public:ThreadData(const std::string &name, const time_t &ctime, func_t f): _name(name), _createtime(ctime), _func_t(f){}public:string _name;time_t _createtime;func_t _func_t;
};void Print()
{std::cout << "我是线程执行的大任务的一部分" << std::endl;
}void *start_routine(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);int cnt = 10;while (cnt--){cout << "I am a new thread, my name is : " << td->_name << " creatname is: " << td->_createtime << endl;td->_func_t();// return nullptr;  //线程终止// pthread_exit(nullptr);// return (void*)110;sleep(1);}
}int main()
{vector<pthread_t> tids; // 保存线程的tid// 创建一批线程for (int i = 0; i < NUM; i++){pthread_t tid;char threadname[64];snprintf(threadname, sizeof(threadname), "%s: %d", "thread", i + 1);// 创建一个线程数据对象string tdname = threadname;ThreadData *td = new ThreadData(tdname, (time_t)time(nullptr), Print);pthread_create(&tid, nullptr, start_routine, td);tids.push_back(tid);//sleep(1);}sleep(5);for (int i = 0; i < NUM / 2; i++){pthread_cancel(tids[i]);cout << "cancel: " << tids[i] << "success" << endl;}void *retval = nullptr;for (int i = 0; i < NUM; i++){// 线程的等待pthread_join(tids[i], &retval);cout << "join sucess, retval is: ";cout << (long long)retval << endl;//sleep(1);}return 0;
}

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

LeetCode-第14题-最长公共前缀

1.题目描述 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 ""。 2.样例描述 3.思路描述 按字符串数组每个数组的长度&#xff0c;将字符串数组从小到大排序&#xff1b;他们的公共前缀一定小于或等于最长元素长度…

2024年智能驾驶年度策略:自动驾驶开始由创造型行业转向工程型行业

感知模块技术路径已趋于收敛&#xff0c;自动驾驶从创造型行业迈向工程型行业。在特斯拉的引领下&#xff0c;国内主机厂2022年以来纷纷跟随特斯拉相继提出“重感知、轻地图”技术方案&#xff0c;全球自动驾驶行业感知模块技术路径从百花齐放开始走向收敛。我们认为主机厂智能…

2023.3.3周报

目录 摘要 一、文献阅读 1、题目 2、摘要 3、模型架构 4、文献解读 一、Introduction 二、实验 三、结论 二、PINN 一、PINN比传统数值方法有哪些优势 二、PINN方法 三、正问题与反问题 三、PINN实验 一、数学方程 二、模型搭建 总结 摘要 本周我阅读了一篇…

Postman上传文件的操作方法

前言 调用某个接口&#xff0c;测试上传文件功能。一时间不知如何上传文件&#xff0c;本文做个操作记录&#xff0c;期望与你有益。 步骤一、设置Headers key:Content-Type value:multipart/form-data 步骤二、设置Body 选择form-data key:file下拉框选择file类型value&…

STM32(8)NVIC编程

中断源由部分片上外设产生 在misc.h中找&#xff0c;杂项 配置NVIC GPIO和AFIO不能产生中断源&#xff0c;但能通过EXTI&#xff0c;由EXTI产生中断源 NVIC不需要开启时钟&#xff0c;因为NVIC模块位于内核内部&#xff0c;芯片一上电就能工作。 中断响应函数 中断向量表在启…

Java:JVM基础

文章目录 参考JVM内存区域程序计数器虚拟机栈本地方法栈堆方法区符号引用与直接引用运行时常量池字符串常量池直接内存 参考 JavaGuide JVM内存区域 程序计数器 程序计数器是一块较小的内存空间&#xff0c;可以看做是当前线程所执行的字节码的行号指示器&#xff0c;各线程…

Unity 常用的4种灯光、制作镜子、灯光的调用修改数值、

创建灯光时&#xff0c;一般用4种&#xff1a;定向光、点光源、聚光、区域光、 定向光&#xff1a;太阳 点光源&#xff1a;灯泡 聚光灯&#xff1a;手电筒 区域光&#xff1a;烘焙-贴图 灯光选择已烘焙 需要先选择被烘焙的物体&#xff0c;然后再选择Contribute GI 等待进…

java中的set

Set Set集合概述和特点 不可以存储重复元素 没有索引,不能使用普通for循环遍历 哈希值 哈希值简介 是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值 如何获取哈希值 Object类中的public int hashCode()&#xff1a;返回对象的哈希码值。 哈希值的特点 同一个…

分布式ID生成算法|雪花算法 Snowflake | Go实现

写在前面 在分布式领域中&#xff0c;不可避免的需要生成一个全局唯一ID。而在近几年的发展中有许多分布式ID生成算法&#xff0c;比较经典的就是 Twitter 的雪花算法(Snowflake Algorithm)。当然国内也有美团的基于snowflake改进的Leaf算法。那么今天我们就来介绍一下雪花算法…

计算机视觉基础知识(二)---数字图像

像素 像素是分辨率的单位;构成位图图像的最基本单元;每个像素都有自己的颜色; 图像分辨率 单位英寸内的像素点数;单位为PPI(Pixels Per Inch),为像素每英寸;PPI表示每英寸对角线上所拥有的像素数目:,x:长度像素数目,y:宽度像素数目,Z:屏幕大小;屏幕尺寸(大小)指的是对角线长…

springer模板参考文献不显示

Spring期刊模板网站&#xff0c;我的问题是23年12月的版本 https://www.springernature.com/gp/authors/campaigns/latex-author-support/see-where-our-services-will-take-you/18782940 参考文献显示问好&#xff0c;在sn-article.tex文件中&#xff0c;这个sn-mathphys-num…

数据结构c版(3)——排序算法

本章我们来学习一下数据结构的排序算法&#xff01; 目录 1.排序的概念及其运用 1.1排序的概念 1.2 常见的排序算法 2.常见排序算法的实现 2.1 插入排序 2.1.1基本思想&#xff1a; 2.1.2直接插入排序&#xff1a; 2.1.3 希尔排序( 缩小增量排序 ) 2.2 选择排序 2.2…

rtt的io设备框架面向对象学习-io设备管理层

目录 1.设备基类2.rtt基类2.1 rtt基类定义2.2 对象容器定义2.3 rtt基类构造函数 3.io设备管理接口4.总结 这层我的理解就是rtt基类和设备基类所在&#xff0c;所以抽离出来好点&#xff0c;不然每个设备类都要重复它。 1.设备基类 /include/rtdef.h中定义了设备基类struct rt_…

STM32(11)按键产生中断

1.初始化IO引脚&#xff0c;设置模式&#xff0c;速度等 2.设置AFIO&#xff08;配置EXTI的引脚映射&#xff09;&#xff0c;记得开启时钟 3.配置EXTI的通道&#xff08;EXTI0和EXTI1&#xff09; 4.配置NVIC 4.1 中断优先级分组 4.2 配置中断 5.编写中断响应函数 在中断向量…

蓝桥ACM培训-实战1

前言&#xff1a; 今天老师没讲课&#xff0c;只让我们做了一下几道题目。 正文&#xff1a; Problem:A 小蓝与操作序列&#xff1a; #include<bits/stdc.h> using namespace std; stack<int> a; int main(){int n,flag1,ans;string cz;cin>>n;for(int i1;…

访问修饰符、Object(方法,使用、equals)、查看equals底层、final--学习JavaEE的day15

day15 一、访问修饰符 含义&#xff1a; 修饰类、方法、属性&#xff0c;定义使用的范围 理解&#xff1a;给类、方法、属性定义访问权限的关键字 注意&#xff1a; ​ 1.修饰类只能使用public和默认的访问权限 ​ 2.修饰方法和属性可以使用所有的访问权限 访问修饰符本类本包…

JetCache源码解析——API实现(持续更新中……)

在JetCache中不仅可以通过在类和接口的函数上使用注解Cached、CacheUpdate和CacheInvalidate等实现缓存加载、更新和删除操作&#xff0c;也支持通过调用API接口的形式来实现缓存的加载、更新和删除操作。 缓存接口 缓存接口的定义如下&#xff1a; /*** 缓存接口&#xff0…

【计算机网络】HTTPS 协议原理

https 一、HTTPS 是什么二、加密1. 加密概念2. 加密的原因3. 常见的加密方式&#xff08;1&#xff09;对称加密&#xff08;2&#xff09;非对称加密 三、数据摘要(数据指纹)四、HTTPS 的工作原理探究1. 只使用对称加密2. 只使用非对称加密3. 双方都使用非对称加密4. 非对称加…

Linux:kubernetes(k8s)部署CNI网络插件(4)

在上一章进行了node加入master Linux&#xff1a;kubernetes&#xff08;k8s&#xff09;node节点加入master主节点&#xff08;3&#xff09;-CSDN博客https://blog.csdn.net/w14768855/article/details/136420447?spm1001.2014.3001.5501 但是他们显示还是没准备好 看一下…

面试笔记系列五之MySql+Mybaits基础知识点整理及常见面试题

目录 Myibatis myibatis执行过程 mybatis的优缺点有哪些&#xff1f; mybatis和hibernate有什么区别&#xff1f; mybatis中#{}和${}的区别是什么&#xff1f; 简述一下mybatis插件运行原理及开发流程&#xff1f;&#xff08;插件四大天王&#xff09; mybatis的mapper没…