【1++的Linux】之线程(一)

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

文章目录

  • 一,Linux线程概念
  • 二,线程的优缺点
    • 进程和线程类比现实
  • 三, 线程的操作
    • 线程的私有资源 && 线程的创建
    • 线程的等待
    • 线程终止
    • 线程分离
  • 四,如何理解线程id

一,Linux线程概念

1. 什么叫做线程?

在官方的定义中:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
也就是说,线程在进程中运行,是OS调度的基本单位。

2. 类比进程对线程再做理解

线程在进程中运行,所以一个进程中可能有多个线程,那么要不要将这些线程管理起来???答案是要的,所以,先描述,再组织。用特定的数据结构将线程管理起来,在一般的OS系统中,我们将这种结构称为TCB,但是Linux中却不是这样实现的。

我们分多个视角来看Linux下的解决方案:
CPU视角: 我们的CPU只认PCB结构体,我拿到一个PCB结构体,我就执行其所对应的代码和数据。我并不关心这是进程还是其他所谓的定义给用户的一些概念。
我们以前所 认识的进程都是只有一个执行流的进程,其内部也可以有多个执行流,这每个执行流我们其实就可以称为线程。我们上面说过,我们的CPU只认PCB,也可以说是,CPU认为一个PCB就是一个执行流。那么每个线程就就需要一个 PCB来进行管理,在其他的一些系统中,这个PCB叫做TCB,但在Linux中,不是这样做的,Linux认为,既然你的大多是结构都和进程类似,那么我可不可以就把你当作一个" 进程 "呢?答案是可以的,这样我就可以简化我OS的复杂度。

那么怎么将程当作一个线程呢?
我们再来回顾以下线程的概念:线程在进程中执行,也就是说,线程在进程的地址空间中运行,一个执行流我们其实就可以称为一个线程。那么我们只需在创建一个特殊的进程,能够与进程共享地址空间既可以,再 通过特殊的手段将当前进程的资源分配给这些特殊的进程,我们将这种特殊的进程就可以称作线程。
在这里插入图片描述
在CPU视角,Linux下PCB<=其他OS下的PCB 为什么呢?
因为linux并没有为线程准备特定的数据结构。在内核看来,只有进程而没有线程,在调度时也是当做进程来调度。linux所谓的线程其实是与其他进程共享资源的进程。为什么说是轻量级?在于它只有一个最小的执行上下文和调度程序所需的统计信息。他是进程的执行部分,只带有执行相关的信息。

内核的视角: 进程是分配系统资源的实体,那么线程就是承担进程一部分资源的实体,进程划分资源给线程。

我们的CPU所调度的实际上是线程,因此线程也是CPU的基本调度单位

因此Linux下的进程统一称为:轻量级进程

Linux下并没有真正意义上的线程,而是用进程的PCB去模拟实现线程。我创**建一个线程和我的父进程挂接到同一份地址空间上,然后在分配资源,分配完还要管理,管理完进行释放。**所以对用户特别不友好!这就是linux用进程去模拟线程的缺点。所以OS并不能直接给我们线程相关的接口,只能提供轻量级进程的接口。但是我们普通用户使用起来是有难度的,因此在用户层又实现了一套用户级的线程接口(以第三方库的形式)。

linux中vfork系统调用,它的作用就是创建一个进程,但是这个进程和父进程共享地址空间。

二,线程的优缺点

优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

下面我们对上面的某些观点进行解释:

为什么说创建一个新的线程代价要比进程小的多: 因为进程之间是独立的,创建一个新进程,就必须要创建新的地址空间,页表,并将代码和数据进行映射。而创建一个线程,我们只需要创建一个新的PCB,并将对应进程中的部分资源划分给线程。

为什么说与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多: 我们的执行流在切换时不仅仅是把在CPU和寄存器中的数据(也就是当前线程的上下文数据)切下来,再把新的线程的数据和代码拿上去跑。我们的计算机为了提高效率,在内存和CPU之间还有多级缓存,从内存中读取数据时,并不是只读自己想要的部分。而是读取足够的字节来填入缓存中。
在这里插入图片描述
在切换线程时,由于我们的线程之间是共享地址空间的,因此多级缓存中的东西就不需要去切换,但是在切换进程时,我们的进程又自己独立的进程地址空间,因此缓存中的东西必须都得切换掉。

缺点

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

性能损失具体是指什么:
若我们有一个非常大的计算任务分给多个线程去做,且我们的线程要比可用的处理器要多,此时我们的某个线程执行到一般被切了下去,让另一个线程去执行,此时就会比让这个线程一直执行计算任务效率低的多。

进程和线程类比现实

说了这么多进程和线程的概念,那么我们可不可以将进程和线程类比现实中的东西呢?答案是—可以的!!!

我们将OS类比这个社会,在这个社会中承担分配资源的实体是一个个家庭,每个家庭都有自己的房子,家庭和家庭之间互相独立,偶尔可以串门进行通信,家庭里有一个个成员,大家各司其职,为了过更好的日子而努力。一个家庭就是一个进程,家庭中的各个成员就好比线程。在这个家庭中,沙发,电视机,冰箱等都是共享的,我们每个成员都可以使用,但是也有一些你自己私有的东西,其他成员是不能看的。
线程出现的目的是为了提高我们的效率,当一个执行流阻塞时,另一个执行流可以接着干其他的事情,还可以是,有多个处理器时我们的多执行流可以同时运行。就好比一个家庭中,一个认在做饭,另一个人可以去打扫卫生,而不是让一个人做完饭再去打扫卫生,这样效率就非常低了。

三, 线程的操作

线程的私有资源 && 线程的创建

由于线程在进程中执行,线程和进程是共享地址空间的,所以其大部分资源都是可以共享的,但也有部分资源是私有的。

  1. 栈:体现的就是每个线程在运行形成的临时数据是可以被压栈入栈的,线程和线程之间临时数据不会互相干扰。
  2. 上下文(一组寄存器):调度上下文,因为一个线程是调度的基本单位,所以一定会形成自己在CPU寄存器中的临时数据,线程是调度的基本单位,必须要有独立的上下文。
  3. 线程ID
  4. errno
  5. 信号屏蔽字
  6. 调度优先级

下面我们对上述的部分说法进行验证:
在这里插入图片描述
该函数是用户级的线程库给我们提供的一个创建线程的接口,

一个新的线程,在编译链接的时候需要引入pthread这个库 。
第一个参数:创建线程的id
第二个参数:线程属性
第三个:线程的回调属性,意味着你要让你的线程执行你代码的哪一部分
第四个参数:给这个回调函数传入的参数

返回值
在这里插入图片描述
我们来看一段代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>using namespace std;void* thread_new(void* args)
{while(true){cout<<"I am new thread"<<endl;sleep(1);}
}int main()
{pthread_t id;int n=pthread_create(&id,nullptr,thread_new,(void*)"thread 1");while(true){cout<<"I am main thread"<<endl;sleep(1);}return 0;
}

在这里插入图片描述
在这里插入图片描述
我们可以看到确实有两个执行流在运行。ps -aL可以查看轻量级进程。LWP是轻量级进程的id。
我们可以看到其线程pid确实是私有的。我们还可以观察到,其进程pid是一样的,并且有一个线程pid和进程pid是一样的,该线程我们叫做主线程。

谁调用这个函数,就获取谁的线程id。
在这里插入图片描述

void* thread_new(void* args)
{while(true){cout<<"I am new thread"<<"我的id是"<<pthread_self()<<endl;sleep(1);}
}int main()
{pthread_t tid;int n=pthread_create(&tid,nullptr,thread_new,(void*)"thread 1");while(true){cout<<"I am main thread"<<"我创建的线程id "<<tid<<endl;sleep(1);}return 0;
}

在这里插入图片描述
线程的健壮性是有问题的,接下来我们进行验证:

void* thread(void* args)
{if(args==(void*)4){int count=3;while(count--){sleep(1);}//除0int a=1;a/=0;}else{while(true){cout<<"I am new thread"<<" id :"<<pthread_self()<<endl;sleep(1);}}}
int main()
{pthread_t  tid[5];for(int i=0;i<5;i++){pthread_create(tid+i,nullptr,thread,(void*)i);}while(true){cout<<"............................."<<endl;cout<<"I am main thread"<<endl;cout<<"............................."<<endl;sleep(1);}return 0;
}

在这里插入图片描述

我们发现程序在三秒后就因为除0错误而退出了,这也说明只要有一个线程出问题,整个进程都得要退出。也说明了线程的健壮性不强。

线程的等待

和进程一样,线程也是需要主线程等待的,否则就会形成僵尸,从而造成内存泄漏。

在这里插入图片描述

第一个参数: 你要等那个线程,传的是线程id。
第二个参数:输出型参数,用来获取新线程退出的时候,函数的返回值。因为你的线程执行函数的返回值是void*,我要以参数的形式把你的返回值拿出来,我就必须是void**。

一个执行流的执行结果有三种情况:代码跑完,结果正确,代码跑完,结果不正确,异常。前两种情况我们在前面学习进程中可以以退出码的形式观察到。但在线程中我们使用的是返回值。

下面是相关代码演示:

void * thread(void* args)
{int count=5;while(count--){sleep(1);cout<<"I am new thread"<<endl;sleep(1);}return (void*)3;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,thread,(void*)"thread 1");void* retval=nullptr;pthread_join(tid,&retval);cout<<retval<<endl;return 0;
}

在这里插入图片描述

我们发现主线程确实接收到了新线程的返回值,并且是阻塞式等待。

那么对于代码异常这种情况,pthread_join能或者需要处理吗?

根本不需要,因为线程出现异常是进程的问题。线程出现问题,主线程根本管不了。某个线程出现问题退出了,那么整个进程就会退出。

线程终止

  1. 从线程函数return.(a.main函数退出return的时候代表进程退出(进程退出又叫主线程退出)b.其他线程函数return ,只代表当前线程退出)
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
    在这里插入图片描述
    在这里插入图片描述
    下面是相关代码演示:
void * thread(void* args)
{int count=5;while(count--){sleep(1);cout<<"I am new thread"<<endl;sleep(1);}pthread_exit((void*)3);//return (void*)3;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,thread,(void*)"thread 1");void* retval=nullptr;pthread_join(tid,&retval);cout<<retval<<endl;return 0;
}

在这里插入图片描述

void * thread(void* args)
{int count=5;while(count--){sleep(1);cout<<"I am new thread"<<endl;sleep(1);}// pthread_exit((void*)3);return (void*)3;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,thread,(void*)"thread 1");void* retval=nullptr;while(1){sleep(3);pthread_cancel(tid);sleep(2);break;}return 0;
}

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

我们可以观察到新创建的进程确实退出了,并且退出码位-1。

这里的-1就是PTHREAD_CANCELED。所以以后我们发现一个线程的退出码是-1,就证明当前线程是被取消的 。

线程有程序替换吗?
线程也可以调用程序替换,但是多线程中所有的代码和数据都是被线程共享的,如果其中有一个线程执行了程序替换,就直接影响到了其他的线程,所以在大部分情况下,很少让线程去调用程序替换,除非你让线程创建子进程再程序替换。一般程序替换和进程强关联。所以不考虑线程的程序替换。

线程分离

线程有程序替换吗?
以上等待线程是阻塞等待,如果我们不想等呢?
线程分离,分离之后的线程不需要被join,运行完毕之后,会自动释放Z状态的pcb,不需要我们等了。这个功能类比于进程中signal(SIGCHLD,SIG_IGN)直接忽略掉。所谓的分离只是设置线程的一种状态表示它不需要被等。就像我们在前面进行的类比,将进程比作一个家庭,线程是家庭中的一个成员,线程分离好比,你仍然在这个家住,但没人在去管你,但是,你要是出现异常退出了,那么整个家也会受到影响。

在这里插入图片描述

下面进行演示:

void * thread(void* args)
{pthread_detach(pthread_self());int count=5;while(count--){cout<<"I am new thread"<<endl;sleep(1);}return (void*)3;
}int main()
{pthread_t tid;int g_id=pthread_self();pthread_create(&tid,nullptr,thread,(void*)"thread");void* retval=nullptr;cout<<"I am main thread"<<endl;sleep(1);int n=pthread_join(tid,&retval);printf("retval:%d---n:%d\n",retval,n);return 0; 
}

在这里插入图片描述
我们可以看到join的返回值不为0,则表示等待失败。

一个线程被设置为分离后,绝对不能在进行join了。使用场景:主线程不退出,并且主线程不关心新线程的返回结果,新线程处理业务处理完毕后自行退出。

四,如何理解线程id

我们会发现线程id和LWP是不一样的,这是为什么呢?
我们查看到的线程id是pthread库的线程id,不是linux内核中的LWP,pthread库的线程id是一个内存地址。这个内存地址是一个虚拟地址。

那么我们又该如何理解线程id呢?
线程在进程中运行,在进程中通过pthread_create创建一个新的进程,这个函数是谁提供的?–是我们的第三方库pthread库提供的,这个库在磁盘中,当我们要使用时,其被加载到内存中,被映射到当前进程的地址空间中的共享区,若有多个进程想要访问,我物理内存中只需要有一份,就都映射到自己的共享区就可以了。
每个线程在运行时,都会有自己的上下文数据,因此,每个线程都需要自己的私有栈来保存这些上下文数据,我的线程要被管理,虽然线程状态等信息在内核的LWP中,但在用户层也要获得相关的属性,这些属性就要放在栈中,但我们用户级栈只有一个,不可能让多个线程去共享,这样使得数据混乱或者被覆盖。这个栈是主线程私有的栈。那么新线程的栈从哪里来呢?我们的用户级线程是由pthread库帮我们创建的,那么新线程的栈也应由它来为我们维护,这个库在我们的共享区,其就会在共享区为我们开辟新线程私有的栈,我们只需要拿到这个栈在地址空间的起始地址,就可以访问这个栈中的代码和数据了。因此将这个虚拟地址作为了我们用户层面所看到的线程id。它与内核中的LWP是一 一对应的。
这叫做用户级线程1:1式的和内核轻量级进程进行1:1对应,这就是linux实现线程的方案。

在这里插入图片描述

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

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

相关文章

chromedp库编写程序

步骤1&#xff1a;首先&#xff0c;我们需要导入chromedp库&#xff0c;以便使用它来下载网页内容。 import chromedp 步骤2&#xff1a;然后&#xff0c;我们需要创建一个函数&#xff0c;该函数接受一个URL作为参数&#xff0c;并使用chromedp库下载该URL的内容。 func do…

关于Vue使用props传值遇到的一些问题

一、The data property “tableData” is already declared as a prop. Use prop default value instead. 翻译过来&#xff1a;数据属性“tableData”已声明为prop。请改prop默认值。 将父组件的prop传过去变量改一下 二、prop传值&#xff0c;子组件比父组件先渲染&#…

LV.12 D17 中断控制器 学习笔记

一、中断控制器 在处理IRQ的时候&#xff0c;会将CPSR写入IRQ_SPSR&#xff0c;然后将CPU切换为IRQ模式&#xff0c;把状态改成ARM状态&#xff0c;把I位写成1禁止全部的IRQ&#xff0c;所以中断这样是我们不想要的。4412是一个四核的CPU&#xff0c;在发送中断前要确定发送给哪…

多测师肖sir_高级金牌讲师_jenkins搭建

jenkins操作手册 一、jenkins介绍 1、持续集成&#xff08;CI&#xff09; Continuous integration 持续集成 团队开发成员每天都有集成他们的工作&#xff0c;通过每个成员每天至少集成一次&#xff0c;也就意味着一天有可 能多次集成。在工作中我们引入持续集成&#xff0c;通…

0基础2小时搭建自己的网站

​作者主页 &#x1f4da;lovewold少个r博客主页 ⚠️本文重点&#xff1a;0基础2小时搭建个人网站 &#x1f449;【C-C入门系列专栏】&#xff1a;博客文章专栏传送门 &#x1f604;每日一言&#xff1a;宁静是一片强大而治愈的神奇海洋&#xff01; 目录 前言 第一步 环境…

Vue-SplitPane可拖拽分隔面板(随意拖动div)

npm install vue-splitpane一、使用 &#xff08;1&#xff09;局部使用&#xff1a; 在vue文件中 import splitPane from vue-splitpane export default {componnets: { splitPane } }&#xff08;2&#xff09;全局使用&#xff1a; 在main.js文件注册 import splitPane…

perl列表创建、追加、删除

简介 perl 列表追加元素 主要是通过push和unshift函数来实现。其中&#xff0c;push是追加到列表尾&#xff0c;unshift是追加到列表头。 perl列表删除元素 主要是通过pop和shift函数来实现。其中&#xff0c;pop是从列表尾删除一个元素&#xff0c; shift是从列表头删除一…

【教3妹学编程-算法题】最大单词长度乘积

3妹&#xff1a;哇&#xff0c;今天好冷啊&#xff0c; 不想上班。 2哥&#xff1a;今天气温比昨天低8度&#xff0c;3妹要空厚一点啊。 3妹 : 嗯&#xff0c; 赶紧把我的羽绒服找出来穿上&#xff01; 2哥&#xff1a;哈哈&#xff0c;那倒还不至于&#xff0c; 不过气温骤降&…

2000-2022年“宽带Z国“试点城市名单匹配数据

2000-2022年“宽带Z国“试点城市名单匹配数据 1、时间&#xff1a;2000-2022年 2、指标&#xff1a;行政区划代码、年份、地区、所属省份、所属地域、试点城市、最早试点年份、DID 3、来源&#xff1a;来自工信部和国家发改委在2014年、2015年和2016年分别遴选的“宽带中国”…

11月起,33个省份纳入数电票开票试点范围内,发票无纸化已是大势所趋!

10月底&#xff0c;北京、贵州、山东&#xff08;不含青岛市&#xff09;、湖南、宁夏5个地区相继发布开展数电票试点工作的通知&#xff0c;至此&#xff0c;全国已有33个省份纳入数电票开票试点范围内。根据上述5地区发布的相关公告&#xff0c;11月1日将正式推行“数电票”开…

python之pyQt5实例:几何绘图界面

使用PyQt5设计一个界面&#xff0c;其中点击不同的按钮可以在画布上画出点、直线、圆和样条曲线 from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QHBoxLayout,QVBoxLayout,QWidget,QLabel from PyQt5.QtGui import QPainter, QPen, QColor from PyQt5.Q…

PyQt5中QLineEdit、QRadioButton、QComboBox这些小部件的change事件

最近在用PyQt5做项目&#xff0c;总结一下QLineEdit、QRadioButton、QComboBox这些部件用到的change事件绑定&#xff0c;即信号与插槽。 QLineEdit QLineEdit 对象是最常用的输入字段。 它提供了一个框&#xff0c;可以在其中输入一行文本。 要输入多行文本&#xff0c;需要…

链式前向星模板

建稠密图可以用邻接矩阵&#xff0c;但稀疏图再用邻接矩阵就很浪费空间了&#xff0c;有可能会爆空间复杂度。 可以用邻接表来实现邻接表建图&#xff0c;两种方法&#xff1a;1.链表 2.链式前向行 只讲第二种&#xff0c;比较常用简洁 链式前向星模板 #define IOS ios::syn…

基于单片机的胎压监测系统的设计

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、系统整体设计方案二、 系统设计4.1 主流程图 三 系统仿真5.1 系统仿真调试实物 四、 结论 概要 本文以STC89C52单片机为控制核心&#xff0c;通过气压传感器模块对汽车各轮胎的胎压进行实时数据的采集与处理&…

创建百科词条 烘托人物形象 提升形象力

百度百科作为中国最大的在线百科全书&#xff0c;小马识途营销顾问认为其具有巨大的商业价值。 首先&#xff0c;百度百科作为百度搜索引擎的一部分&#xff0c;吸引了数以亿计的用户访问&#xff0c;这为百度提供了大量的广告展示和点击收入的机会。 其次&#xff0c;百度百科…

20231106-前端学习加载和视频球特效

加载效果 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>加载效果</title><!-- 最新…

freertos多任务

以前我们都是一个任务&#xff0c;假设现在我们创建三个任务,项目工程在上一节网盘 #include "stm32f10x.h" // Device header #include "freertos.h" #include "task.h" #include "usart.h"TaskHandle_t myTaskHan…

Vue纯CSS实现掷色子

效果图&#xff1a; 实现代码 直接利用CSS3动画实现的效果&#xff0c;无js代码。 <template><div class"wrap"><input type"checkbox" id"roll"><label for"roll"><div class"content"><…

音视频报警可视对讲15.6寸管理机

音视频报警可视对讲15.6寸管理机 一、管理机技术指标&#xff1a; 1、15.6寸原装京东方工业液晶触摸屏&#xff0c;分辨率1920 (H) x 1080 (V)&#xff1b; 2、1000M/100M自适应双网口&#xff1b; 4、按键设置&#xff1a;报警/呼叫按键&#xff0c;通话/挂机按键&#xff…

Android Studio(列表视图ListView)

前言 前面在适配器章节&#xff0c;已经介绍了ListView的作用(干什么的)&#xff0c;这节将主要介绍如何去设计ListView页面视图。 思考 列表视图需要些什么&#xff1f; 1. 列表项容器&#xff08;装载各列表项的容器&#xff09;&#xff1a;<ListView/> 2. 列表项布局…