线程的概念和控制

news2025/1/12 12:02:43

文章目录

  • 线程概念
    • 线程的优点
    • 线程的缺点
    • 线程异常
    • 线程用途
    • 理解虚拟地址
  • 线程控制
    • 线程的创建
    • 线程终止
    • 线程等待
    • 线程分离
    • 封装线程库

线程概念

什么是线程?

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

如何看待之前的进程?
之前的进程是内部只有一个执行流。

如何看待现在的进程?
现在的进程内部有多个执行流。并且多个执行流共享大部分资源。

线程更像是一种标准,各个平台的实现方式可能不同,但是作用都是一样的。在Linux中,因为线程也是执行流,进程也是,并且一个进程内的所有线程共享大部分资源。所以Linux中线程的实现就直接复用了进程的代码,这样在OS的调度算法就只有一个进程调度就可以了,一个进程中的的线程是共享大部分数据,所以创建线程可以直接复制PCB就可以了,一个进程中是可以存在多个线程的,所以OS也一定会对线程进行管理,所以OS也一定要有对线程描述的结构体(TCB),但是线程是直接复制进程的,所以Linux中描述线程的结构体也是PCB。所以Linux下线程也称为轻量级进程。
在这里插入图片描述
因此现在看来,线程是CPU调度的基本单位,进程就是承担系统资源的基本实体。

线程的优点

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

为什么说创建线程比进程的代价小呢呢?
因为线程是在进程的地址空间中运行的,并且线程创建更简单,只需要复制进程的PCB,只有一小部分的数据是私有的,大部分数据都和进程是一样的。

线程切换的效率为什么高?
如果是一个进程中的两个线程进程切换的话,CPU中的有一部分寄存器中的内容是不需要被切换的,并且因为局部性原理,CPU中是存在Cache缓存的,如果是一个进程中的两个线程进程切换,根据局部性原理Cache缓存也大部分不会被替换,但是如果是进程切换,所有的寄存器和Cache都是要被切换的。

线程的缺点

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

线程异常

  1. 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  2. 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
    进程内的所有线程也就随即退出

线程用途

  1. 合理的使用多线程,能提高CPU密集型程序的执行效率
  2. 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

我们说线程和线程之间大部分数据是共享的但是有一部分数据是私有的,那么什么共享什么私有?
共享

文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
地址空间

私有

线程ID(lwp)
一组寄存器

errno
信号屏蔽字
调度优先级

理解虚拟地址

我们现在直到磁盘中文件是以4KB为单位存储的,称之为页帧。并且我们编译好的可执行程序仍然遵守这样的规则,所以我们的内存空间也是被划分为4KB大小为单位的空间,称之页框,所以在访问一块内存时只需要知道页框的首地址+页内偏移就可以访问内存中的任意一个地址空间。因为内存会被划分成很多的页框,所以OS要对内存管理,就需要先描述在组织,可以理解为所有的页框都被放在一个数组中,然后OS对内存的管理就变成了对数组的增删查改。

虚拟地址到物理地址的转换是需要页表的,页表的每一行存在很多的字段,假设现在是10个字节,要是每个物理地址都存在一个虚拟地址跟他直接映射的话,假设是2^32的内存,就需要40G来存放页表,显然是不可能的,所以虚拟地址和物理地址并不是直接进行映射的。
以32为的地址为例假设先现在有一个地址 11110011 10111011 00101001 10100101 一个32个比特位,把前10 为1111001110作为一个整体,一共10个比特位,可以表示的范围就是0~1023,所以假设有一个1024大小的数组,就可以通过前十位的数据找到一个数组的下标,数组的内容还是一个大小为1024的数组,这个数组为页目录,然后11 ~ 20为比特位1110110010作为数组指向的那个数组的下标,数组的内容就是页框的起始地址,然后最后12个比特位就是页内的偏移地址。所以通过这样的方式找到物理地址,并且大大的减少了直接映射的使用空间,因此在页表中是没有物理地址的,在CPU中有一个MMU寄存器,我们只需要把一个虚拟地址放进去,就可以值就拿到物理地址然后进行访问。当然CPU中也有一个寄存器专门保存的就是当前页目录的起始地址。

在这里插入图片描述
每个线程要执行自己的代码,根据我们传递的函数,本质就是划分页表,划分页表的本质就是划分地址空间。所以在进程的视角,虚拟地址空间本身就是资源。

进程和线程关系如下:
在这里插入图片描述

线程控制

Linux中是没有真正的线程的,只有轻量级进程的概念,所以OS只会提供轻量级进程的系统调用,不会直接提供线程调用的接口。所以为了便于人们对线程的控制,写Linux的程序员就把对线程的控制封装成了pthread原生线程库。对上提供线程控制的接口。

  1. 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  2. 要使用这些函数库,要通过引入头文<pthread.h>
  3. 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

线程的创建

在这里插入图片描述

  1. 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  2. pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

在Linux中可以通过ps -aL 查看创建的线程
在这里插入图片描述
我们可以看到同个进程内的线程的pid是相同的,但是LWP是不同的,因为LWP是线程的id,LWP在内核中使用,和我们用pthread_create获取出来的线程id是不一样的。内核中用LWP来表示线程的唯一。

pthread_create获取出来的线程id是我们用户自己使用的,可以通过pthread_ self()来获取。
在这里插入图片描述
那么这个线程id到底是什么呢?
我们使用的所有的线程的函数都不是系统直接提供的,是原生线程库提供的,而原生线程库一定不只会有我们一个进程用,所以原生线程库中一定会存在多个进程创建的多个线程,所以线程库一定要把我们多个进程创建的线程给管理好,所以线程库中会存在描述线程的结构体,结构体中有很多线程的数据(属于哪个进程,线程id等),然后再用数据结构把各个描述线程的结构体管理起来。我们来认识一个系统调用:
在这里插入图片描述
它可以通过flags的标识符来表示创建一个进程或者是创建一个轻量级进程(线程),我们看到参数中有一个child_stack的参数,表示我们是可以传一段空间是作为线程的栈空间的,所以我们前面说每个线程有自已的独立栈空间,pthread_create的底层就是封装了这个函数。因此我们每个新线程都会有自己的栈空间,而默认地址空间中的栈由主线程使用。在原生线程库中每个线程和每个线程的数据结构和栈空间还有一些相关的独立的数据放在一起,而我们用户用的线程id就是线程属性在线程库中的地址。
在这里插入图片描述

现在理解了线程id后,我们迷惑的应该是线程的局部存储是什么,我们知道对于全局变量来说是被所有线程共享的,但是加了一个__thread修饰一个变量,程序在编译的时候就会为每个线程开辟一段空间专门存储这个变量,也就是说,这个变量每个线程都存在一份,互不干扰。
在这里插入图片描述

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit
在这里插入图片描述
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel
在这里插入图片描述

线程等待

为什么要进程线程等待?

  1. 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  2. 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join

在这里插入图片描述
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED(-1)。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
    数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

线程分离

一般情况下对于创建的线程我们是需要join的,但是如果我们不关系线程的返回值,那么join就会成为一中负担,这时我们就可以对线程进程分离。即当线程退出时,自动释放线程资源。

pthread_detach

在这里插入图片描述
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,可以通过pthread_self()来获取自己的线程id。

join和分离是冲突的,一个线程不能既是join又是分离的。

如何理解语言中的线程库?
本质就是对原生线程库的封装。

线程中可以进程fork吗?可以进程execl程序替换吗?
线程中是可以fork的,也是可以进程execl程序替换的,但是进行程序替换整个进程的代码都会被替换,可能会影响其他线程的正常运行,比较推荐先fork然后在进程程序替换。

封装线程库

基于上面的接口,我们来模拟实现一下简单版的线程库。

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>

template<class T>
using func_t = std::function<void(T)>;

template<class T>
class Thread
{
public:
    Thread(const std::string& name, func_t<T> func, T data) : _name(name), _func(func), _tid(0), _isruning(false), _data(data)
    {}

    static void* threadRountine(void* attr)
    {
        Thread* t = static_cast<Thread*>(attr);
        t->_func(t->_data);
    }

    void Start()
    {
        int n = pthread_create(&_tid,nullptr,threadRountine,this);
        if(n == 0) 
        {
            _isruning = true;
        }
        else 
        {
            std::cerr << "pthread error" << std::endl;
        }
    }

    void Join()
    {
        if(!_isruning) return;
        int n = pthread_join(_tid,nullptr);
        if(n == 0)
        {
            _isruning = false;
        }
        else 
        {
            std::cerr << "join error" << std::endl;
        }
    }

    std::string getname()
    {
        return _name;
    }

    bool isruning()
    {
        return _isruning;
    }
private:
    std::string _name;
    pthread_t _tid;
    bool _isruning;
    func_t<T> _func;
    T _data;
};

如果需要返回值可以在成员变量可以加个模板参数在成员变量中定义一个返回值通过join得到就可以,如果调用的函数参数有多个也可以通过类似的方法实现。

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

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

相关文章

kali模块及字典介绍

1. 基本模块介绍 模块 类型 使用模式 功能 dmitry 信息收集 命令行 whois查询/子域名收集/端口扫描 dnmap 信息收集 命令行 用于组建分布式nmap&#xff0c;dnmap_server为服务端;dnmap_client为客户端 i…

踩坑——纪实

开发踩坑纪实 1 npm安装1.1 查看当前的npm镜像设置1.2 清空缓存1.3 修改镜像1.4 查看修改结果1.5 重新安装vue 2 VScode——NPM脚本窗口找不到3 springboot项目中updateById()失效4 前端跨域4.1 后端加个配置类4.2 CrossOrigin注解 5 路由出口6 springdoc openapi3 swagger3文件…

2024.5.21欧洲商会网络安全大会(上海)

本次安策将将参加超越 2024 年网络安全大会&#xff1a;驾驭数字前沿大会(上海)&#xff0c;2024年5月21日&#xff0c;期待和欢迎新老朋友在大会上会面和交流。 时间 2024-05-21 |14:00 - 16:30 场地&#xff1a; 上海瑞士大酒店 地址&#xff1a; 3rd Floor&#xff0c; Davo…

零门槛微调大模型:基于 Ludwig 低代码框架使用 LoRA 技术微调实践

一、Ludwig 介绍 自然语言处理 (NLP) 和人工智能 (AI) 的飞速发展催生了许多强大的模型&#xff0c;它们能够理解和生成如同人类般的文本&#xff0c;为聊天机器人、文档摘要等应用领域带来了革命性的改变。然而&#xff0c;释放这些模型的全部潜力需要针对特定用例进行微调。…

php发送短信功能(创蓝短信)

一、以下是创蓝发送短信的功能&#xff0c;可以直接执行&#xff1a; <?php$phone 12312312312;$msg 测试短信功能;echo 发送手机号&#xff1a;.$phone.<br/>;echo 发送内容&#xff1a;.$msg.<br/>;$send sendMessage($phone, $msg);var_dump($send);…

(一)vForm 动态表单设计器之使用

系列文章目录 &#xff08;一&#xff09;vForm 动态表单设计器之使用 文章目录 前言 一、VForm是什么&#xff1f; 二、使用步骤 1.引入库 2.使用VFormDesigner组件 3.使用VFormRender组件 4.持久化表单设计 总结 前言 前段时间在研究Activiti工作流引擎&#xff0c;结合业务…

群晖搭建网页版Linux Ubuntu系统并实现远程访问

文章目录 1. 下载Docker-Webtop镜像2. 运行Docker-Webtop镜像3. 本地访问网页版Linux系统4. 群晖NAS安装Cpolar工具5. 配置异地访问Linux系统6. 异地远程访问Linux系统7. 固定异地访问的公网地址 docker-webtop是一个基于Docker的Web桌面应用&#xff0c;它允许用户通过浏览器远…

[实例] Unity Shader 逐像素漫反射与半兰伯特光照

漫反射光照是Unity中最基本最简单的光照模型&#xff0c;本篇将会介绍在片元着色器中实现反射效果&#xff0c;并会采用半兰伯特光照技术对其进行改进。 1. 逐顶点光照与逐像素光照 在Unity Shader中&#xff0c;我们可以有两个地方可以用来计算光照&#xff1a;在顶点着色器…

网创教程wordpress插件自动采集并发布

使用教程 出现404的请搞定自己网站的伪静态。一般都是伪静态问题。 需要定制可以联系我。 本次更新主要更新了。界面的设置。用户可以直接设置文章的分类。 设置文章发布的金额。 使插件更加的人性化。优化了采集更新发布的代码。 更新了网站的界面。 主要功能&#xff1a; w…

K8s Service 背后是怎么工作的?

kube-proxy 是 Kubernetes 集群中负责服务发现和负载均衡的组件之一。它是一个网络代理&#xff0c;运行在每个节点上, 用于 service 资源的负载均衡。它有两种模式&#xff1a;iptables 和 ipvs。 iptables iptables 是 Linux 系统中的一个用户空间实用程序&#xff0c;用于…

Ubuntu 20/22 安装 Jenkins

1. 使用 apt 命令安装 Java Jenkins 作为一个 Java 应用程序&#xff0c;要求 Java 8 及更高版本&#xff0c;检查系统上是否安装了 Java。 sudo apt install -y openjdk-17-jre-headless安装完成后&#xff0c;再次验证 Java 是否已安装 java --version2. 通过官方存储库安…

冯喜运:5.24现货黄金趋势解读,黄金原油行情分析及操作建议

【黄金消息面分析】&#xff1a;美国劳工部公布的最新数据显示&#xff0c;截至5月18日的一周内&#xff0c;首次申请失业救济人数下降至21.5万人&#xff0c;创下自去年9月以来的最大降幅。数据公布后&#xff0c;现货黄金短线下挫6美元&#xff0c;报2362.71美元/盎司。这表明…

泰迪智能科技分享:2024年职业院校中职组ZZ052大数据应用与服务赛项赛题第01套【子任务二:Hadoop 完全分布式安装配置】答案

Hadoop完全分布式安装配置 任务内容 本实训需要使用root用户完成相关配置&#xff0c;master、slave1、slave2三台节点都需要安装JDK与Hadoop&#xff0c;具体要求如下&#xff1a; 将JDK安装包解压到/root/software目录下&#xff1b;在“/etc/profile”文件中配置JDK环境变…

游戏联运的挑战与核心关键点

​游戏联运一个看似充满机遇与挑战的行业&#xff0c;吸引了很多创业者的加入。然而&#xff0c;真正踏入这个行业后&#xff0c;许多人会发现&#xff0c;手游代理并非想象中的那么简单。今天&#xff0c;溪谷软件就来和大家聊聊游戏联运是怎么做的&#xff0c;需要注意什么。…

使用RAG和文本转语音功能,我构建了一个 QA 问答机器人

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学. 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 汇总合集&…

VC++学习(3)——认识MFC框架,新建项目,添加按钮

目录 引出第三讲 MFC框架新建项目Windows搜索【包含内容的搜索】如何加按钮添加成员变量添加成功 添加按钮2杂项 总结 引出 VC学习&#xff08;3&#xff09;——认识MFC框架&#xff0c;新建项目&#xff0c;添加按钮 MFC(Microsoft Foundation Classes)&#xff0c;是微软公…

【Linux】关于获取进程退出状态中的core dump标志补充

通过 wait/waitpid 可以获取子进程的退出状态, 从而判断其退出结果. 记录退出状态的 int 变量 status 的使用情况如下图所示: 如果是收到信号终止的话, 低 7 位为收到的终止信号, 而低第 8 位为 core dump 标志, core dump 标志有什么用呢? core dump 标志只存 0/1, 表示是否…

leetcode以及牛客网单链表相关的题、移除链表元素、链表的中间节点、合并两个有序链表、反转链表、链表分割、倒数第k个节点等的介绍

文章目录 前言一、移除链表元素二、链表的中间节点三、合并两个有序链表四、反转链表五、链表分割六、倒数第k个节点总结 前言 leetcode以及牛客网单链表相关的题、移除链表元素、链表的中间节点、合并两个有序链表、反转链表、链表分割、倒数第k个节点等的介绍 一、移除链表元…

解决go install 网络问题

rootiZbp1hiqzlhh6w05gloffgZ:~# go install mvdan.cc/garblelatest go: mvdan.cc/garblelatest: module mvdan.cc/garble: Get "https://proxy.golang.org/mvdan.cc/garble/v/list": dial tcp 172.217.160.81:443: i/o timeout解决方法 更换阿里代理 rootiZbp1hiq…

js——数据操作——实现阶梯价格排序——基础积累

最近在写网络报价的时候&#xff0c;遇到一个需求&#xff0c;就是要根据采购数量&#xff0c;找到符合数量的阶梯区间&#xff0c;并找到最便宜的采购价格。 比如下面&#xff1a; let originViewList [{id:1,incrementalQuantity:10,priceList:[{minQuantity:1,price:20},…