操作系统:线程

目录

前言: 

1.线程

1.1.初识线程

1.2.“轻量化”进程

1.3.线程与进程

2.线程控制 

2.1.pthread原生线程库

2.2.线程控制的接口 

2.2.1.线程创建

2.2.线程退出|线程等待|线程分离|线程取消

2.3.pthread库的原理

2.4.语言和pthread库的关系

2.5.线程局部存储


前言: 

在前面的学习中,我们知道进程是一种处理任务的执行流,操作系统中的大部分任务都由进程来处理,而进程的创建,需要开辟内存来产生进程PCB、进程虚拟地址空间、页表……,而这个进程的创建成本较大,于是操作系统实现了另一种执行流------线程。

  1. 线程是比进程更加轻量化的一种执行流,线程是进程内部的一种执行流。
  2. 线程是CPU调度的基本单位,进程是承担系统资源的实体。

那么我们大概就能猜到:进程是线程的载体,操作系统增加了线程这个新的执行流后,进程的角色变为了在系统中创建、获取资源,用来供给线程执行流,实现CPU对线程的不断调度,即:进程是线程的宏观体现?带着这个猜测,我们开始进入线程的学习……

1.线程

1.1.初识线程

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”

我们在以往的博客中,把进程当做一个执行流来看待,是因为我们在进程中只有一条单一的执行流,当我们在某个进程中创建新的线程之后,就出现了“主线程”和“新线程”,那么这里我们就知道了进程是通过线程来作为执行流的。 

一言以蔽之:对于Linux而言,进程是一个资源的结合体,而线程就是通过这些资源来完成任务的一个个执行流。

比如:有一家大公司,干活的肯定不是大公司,而是公司里的各个部门中的打工人,公司这个进程提供的就是给线程的资源,给线程一个好的平台、环境去发挥。


1.2.“轻量化”进程

在操作系统中,虽然为了解决进程过于笨重的问题,引入了线程的概念,但是线程和进程一样都是一个需要描述的结构,那么进程用process control block(PCB)来描述,那么线程是不是也同样需要TCB这一个数据结构来进行描述呢?

答案:线程也是需要通过数据结构,进行“先描述再组织”的,但是我们实现这个结构可以通过两个方向:1.创建新的TCB体系,实现一份类似于PCB的体系。2.复用当前PCB体系。

对于Windows而言,实现了一个Thread的体系。而Linux则是在PCB的基础上,将线程抽象成“轻量化进程”这个概念,接着复用当前PCB体系……(这里也体现了tast_stuct不完全等于PCB)

如图:对应我们上面所述------Linux对线程的实现是抽象成“轻量化”进程,这是怎么理解的呢?

  1. 首先进程拥有它的内核数据结构、代码和数据,所以在空间中需要开辟较多资源来存储
  2. “轻量化”体现在于,Linux创建新的线程时,只创建一份新的task_struct,和部分的代码和资源,这样子就能够减少资源的开辟
  3. 我们说是创建了新的task_struct,但是实际上这些创建的“轻量化”进程共用着原进程的资源,也就是线程他们是可以访问同一个进程内的数据的。

除了上面的几点,线程的轻量化也体现在CPU的调度上……这里我们需要重点讲解!!!

我们知道:在进程间转换时,CPU在调度不同的进程时需要进行进程相关的上下文切换,以及页表、进程地址空间相关的寄存器内数据的切换……

而线程中的切换,因为访问的是同一个进程的资源,所以大部分的寄存器内容不用修改,这样子线程在CPU切换和调度就显得轻量化了。

实际上:CPU内会维护一块cache缓存,一般情况下CPU是从内存直接读取进程的数据并加载到CPU中,而为了减少IO提高系统效率,所以CPU会将进程的部分代码和数据提前读取进cache中,这部分预加载的代码和数据符合局部性原理。而进程切换,这一块缓存中保存的热数据也需要切换。线程切换并不需要切换cache

面试题:线程切换为什么效率高?

  1. 切换的寄存器少
  2. 不需要重新更新cache缓存 

1.3.线程与进程

  1. 进程是资源分配的基本单位,对于线程而言,进程是线程的载体,给线程提供资源。
  2. 线程是调度的基本单位,宏观上是进程被调度,其实在CPU中是通过线程(LWP)来进行任务的调度的
  3. 线程共享进程数据,但也拥有自己的一部分数据。

对于第三点,因为线程需要被CPU进行调度,处于多线程时,就需要保存当前线程的上下文(类似进程切换),所以线程会维护一份寄存器的结构体数据。并且因为线程会进行函数的跳转,所以内部也需要一个函数栈结构。另外,线程也维护着优先级、线程id等数据……

如图:即为进程与线程的关系,这就回应了我们在前言中的猜测“进程其实就是线程的载体,进程是线程的宏观体现”。

更加详细的进程与线程的关系:线程与进程,你真得理解了吗_进程和线程的区别-CSDN博客 

2.线程控制 

2.1.pthread原生线程库

在LInux中并没有实现线程这一个模块,而是通过轻量化进程来模拟线程,所以Linux操作系统只提供了“轻量化进程”的系统调用,并没有之间创建线程的接口。因此为了适配不同的开发需求,Linux实现了原生的pthread原生线程库,来实现用户级和系统的轻量化进程的适配,也就是实现了类似于上层C++、Java等面向对象语言的线程!

本质上就是:Linux封装了一层,通过内核中的轻量化进程和Pthread库实现了线程,而不是直接就创建线程这个模块,实现Linux操作系统适配多线程! 

如图即为:Linux自带的pthread库,所以我们在使用pthread库时,需要连接这个库!!! 


2.2.线程控制的接口 

2.2.1.线程创建

// 函数原型为pthread_create()
参数分别为:线程tid,栈的地址,调用函数指针,传入参数类型

pthread_create(pthread_t *thread, 
               const pthread_attr_t *attr, 
               void * (*start_routine)(void*), 
               void *arg);

对于进程创建我们需要注意的是:我们需要提前设置tid然后传入,并且传入参数为void*,表示可以传入多种类型的参数,可以是int、string、甚至是自定义的对象!

最基本的进程创建的使用: 

void *ThreadTest1(void *arg)
{
    const char *threadName = (const char *)arg;
    while (1)
    {
        cout << "i am a new thread, mypid is: " << getpid() << ", my name is:" << threadName << endl;
        sleep(1);
        cout << endl;
    };
}
// 线程创建
int main()
{
    // 在main这个进程(主线程)中创建一个新线程
    pthread_t tid;
    // 创建完线程后线程跳转进程ThreadTest函数中
    pthread_create(&tid, nullptr, ThreadTest1, (void *)"Thread one");

    // 主线程
    while (1)
    {
        cout << "my name is Thread main, my pid is: " << getpid() << endl;
        sleep(1);
        cout << endl;
    }
}

 这段代码中我们实现了:两个循环体循环打印各自的内容,这也表示了我们创建了新的执行流,并且他们的进程pid是一致的!

进程创建传入对象参数:

typedef function<void()> func_t;

class ThreadData
{
public:
    ThreadData(const string &name, const uint64_t &ctime, func_t f)
        : thread_name(name), creat_time(ctime), func(f)
    {
    }

    string GetName() const { return thread_name; }
    uint64_t GetTime() const { return creat_time; }
    func_t GetFunc() const { return func; }

private:
    string thread_name;
    uint64_t creat_time;
    func_t func;
};
void Print()
{
    cout << "only print……" << endl;
}
void *ThreadTest2(void *arg)
{
    ThreadData *td = (ThreadData *)arg;
    while (1)
    {
        cout << "当前线程名为:" << td->GetName() << ",创建时间为:" << td->GetTime() << endl;
        (td->GetFunc())();
        sleep(1);
    }
}
// 线程函数是可以传对象作为参数的
int main()
{
    pthread_t tid;
    ThreadData *td = new ThreadData("Thread one", (uint64_t)time(nullptr), Print);

    // 可以传入任意类型的参数
    pthread_create(&tid, nullptr, ThreadTest2, td);

    // 主线程
    while (1)
    {
        cout << "my name is Thread main, my pid is: " << getpid() << endl;
        sleep(3);
        cout << endl;
    }
}

 这段代码的核心和上一个一致,不过这里传入的参数是TreadData这个类的对象,并且在我们给线程完成任务的函数区ThreadTest2中,我们可以接收这个对象并且对这个对象进行操作……实际开发中,线程主要也是通过对传入对象进行操作来实现各种需求的处理的!

2.2.线程退出|线程等待|线程分离|线程取消

线程退出的方式:1.调用的函数完成当前的函数模块,这时会返回nullptr,线程退出。2.通过线程退出函数来实现。另外线程退出不能通过exit函数,exit函数会导致整个进程退出……

// 线程退出函数,填入返回的内容(注意不能返回临时变量)
pthread_exit(void *value_ptr);
// 线程等待函数,传入线程tid,接收的返回值
pthread_join(pthread_t thread, void **value_ptr);

跟进程退出类似,线程退出时也需要主线程进行等待,这里等待的内容主要是“子线程的返回内容”

// 1.不需要返回值!

// ThreadFunc中
pthread_exit(nullptr);

// main中
pthread_join(tid, nullptr);

// 2.需要接收返回值

// ThreadFunc中
pthread_exit((void*)"hello thread");    // 返回值为hello world

// main中
void *ret = nullptr;
pthread_join(tid, &ret);    // 通过指针来接收这个返回值,原理涉及二级指针

 这里对应着线程等待的两种情况,一旦我们使用了pthread_join那么主线程就会进入阻塞等待。而在第一种情况中并不需要我们进行返回值的接收,这时的等待是不必要的!

因此pthread库中实现了线程分离的接口,这个接口主要是适配当我们不需要关心子线程返回值时,又不想对子线程进行等待,我们可以直接分离这个线程,

// 线程是可以设置为分离状态,主线程不用对新线程进行等待
// 可以是主线程对子线程进行分离
// 在main中分离线程
pthread_detach(tid);

// 不过大部分情况下,我们一般在子进程自己的函数块中分离线程
// 也可子线程对自己进行分离,但是写在这里最好
pthread_detach(pthread_self());

 而线程取消接口主要是正常终止掉我们创建的某个线程

// 线程是可以取消的---相当于之间终止该线程
pthread_cancel(tid);

// cancle后通过pthread_join接收该进程的返回值,会返回-1

2.3.pthread库的原理

在Linux操作系统中,并没有线程的概念。我们这里所讲的线程是用户级线程,是通过pthread库来实现的。所以我们在用户层需要对线程进行管理,在Pthread库中我们也需要定义struct TCB这一个结构,来实现先描述再组织!

既然要实现线程控制块,那么我们就需要定义“栈空间”和“寄存器”这些独立的属性,寄存器模块pthread库可以复用进程中维护的寄存器模块,但是栈空间这个模块我们该如何抽象并实现呢? 

背景:首先对于单个进程,只有一个地址空间也就只能开辟出一块栈空间,那么从进程中获取栈空间显然是不合理的。所以我们通过进程来实现线程的栈空间这个方向是无法实现的,我们在之前的学习中,对于用户级别的缓冲区,本身也是一块空间,而这块缓冲区的实现是通过C库的,那么我们也可以通过pthread库来实现这一块栈空间。

clone(int (*child_func)(void *),     // 调用的函数
           void *child_stack,        // 开辟的栈空间
           int flags,                // 创建方式
           void *arg, ...            // 传入参数
          /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

这个接口是Linux操作系统实现线程的底层系统调用,也是pthread_create的底层实现,因此我们知道通过库来实现这个栈空间是可行的! 

我们知道动态库是需要加载进物理内存,并且映射进地址空间,当我们创建一个新线程,就可以把维护的栈空间加载进内存中, 而进程原本的栈空间就是主线程的空间了。


并且当我们动态库加载进物理内存后,而库本身就是pthread实现的代码,当我们在创建线程时,我们从正文代码段跳转到动态库中,运行线程的创建“代码”,那么我们进行线程的管理也是通过动态库的资源(代码)即:进程读取映射进进程地址空间内的动态库的代码,创建数据结构

这时我们也明白了,为什么线程的pthread_t和Linux中的LWP在数值上并不相等!前者是库级别的概念,后者是操作系统的概念。 

2.4.语言和pthread库的关系

我们上面讲述了Linux环境下pthread的实现原理,那么对于C++、Java语言他们内置实现的多线程模块,有什么关系呢。这里我们以c++的thread为例

#include<iostream>
#include<thread>
#include<unistd.h>
#include<cstdlib>

using namespace std;

void ThreadFunc()
{
    while(1)
    {
        cout<< "i am a thread from C++" <<endl;
        sleep(1);
    }
}

int main()
{
    thread t(ThreadFunc);
    t.join();
}

接着我们在Linux环境中编译这个文件: 

  1. 当我们第一次编译并运行时(即为绿框内容),我们发现程序无法运行,并且报错为:程序运行在一个不支持多线程或者多线程被禁用的环境中。但是我们明明包括了C++中提供的线程库!
  2. 但是当我们链接上了Linux提供的动态库时,这个程序又可以正常运行!

 看到这里大家应该明白了:纯C++的接口创造线程时,也是需要Linux的pthread库,本质上就是C++的标准就是对pthread库的封装!即不同的语言实现多线程的本质就是对不同系统实现的多线程的实现进行封装!比如我们在STL容器中实现的Swap函数,内部是通过封装std中swap函数来实现的!这样就实现了语言代码的可移植性!

2.5.线程局部存储

我们在2.3.中的图看到了线程的属性集中维护了一个线程的局部存储模块,这个模块的作用是,设定同一个变量能给不同的线程维护一个各自独立的值。

int g_val = 100;
__thread int t_val = 0;
void *ThreadFunc(void *arg)
{
    while (1)
    {
        g_val += 10;
        t_val--;
        cout << "new thread g_val = " << g_val << ", t_val = " << t_val << endl;
        sleep(2);
    }
}
// 线程的局部存储
void test7()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadFunc, (void*)nullptr);

    while(1)
    {
        t_val++;
        g_val = 0;
        cout << "main thread g_val = " << g_val << ", t_val = " << t_val << endl;
        sleep(2);
        cout<<endl;
    }
}

这段代码中我们定义了一个全局变量g_val和对于线程的__thread的t_val,然后我们各自对g_val和t_val进行修改

通过程序的运行结果:我们发现g_val用的是同一块空间,而t_val用的是不同的空间,那么由于__thread这个关键字,编译器编译时会把这个变量分别加载到线程的局部存储区,也就是同一个变量,在不同的空间维护着,不同的线程可以有独立的t_val,这也就是线程LWP的实现原理……


  

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

redis核心数据结构——跳表项目设计与实现(跳表结构介绍,节点类设计,随机层级函数)

跳表结构介绍。跳表是redis等知名软件的核心数据结构&#xff0c;其实现的前提是有序链表&#xff0c;思想的本质是在原有一串存储数据的链表中&#xff0c;间隔地抽出一半元素作为上一级链表&#xff0c;并将抽提出的元素和原先的位置相关联&#xff0c;这样重复下去直到最上层…

Android AOSP探索之Ubantu下Toolbox的安装

文章目录 概述安装Toolbox解决运行的问题 概述 由于最近需要进军android的framework,所以需要工具的支持&#xff0c;之前听说江湖上都流传source insight,我去弄了一个破解版&#xff0c;功能确实强大&#xff0c;但是作为多年android开发的我习惯使用android studio。虽然使…

数据分析及AI技术在旅游行业的应用

引言 旅游行业是一个充满潜力和机遇的领域&#xff0c;而数据分析和人工智能&#xff08;AI&#xff09;技术的迅猛发展为这个行业带来了前所未有的机遇和挑战。本文将探讨数据分析及AI技术在旅游行业中的具体应用及其带来的影响。 数据分析在旅游行业的4种应用 在旅游行业…

【开源设计】京东慢SQL组件:sql-analysis

京东慢SQL组件&#xff1a;sql-analysis 一、背景二、源码简析三、总结 地址&#xff1a;https://github.com/jd-opensource/sql-analysis 一、背景 开发中&#xff0c;无疑会遇到慢SQL问题&#xff0c;而常见的处理思路都是等上线&#xff0c;然后由监控报警之后再去定位对应…

附录3-小程序常用事件

目录 1 点击事件 tap 2 文本框输入事件 input 3 状态改变事件 change 4 下拉刷新事件 onPullDownRefresh() 5 上拉触底事件 onReachBottom() 1 点击事件 tap 2 文本框输入事件 input 可以使用 e.detail.value 打印出当前文本框的值 我现在在文本框中依次输入12345&…

APScheduler定时器使用:django中使用apscheduler,使用mysql做存储后端

一、基本环境 python版本&#xff1a;3.8.5 APScheduler3.10.4 Django3.2.7 djangorestframework3.15.1 SQLAlchemy2.0.29 PyMySQL1.1.0二、django基本设置 2.1、新增一个app 该app用来写apscheduler相关的代码 python manage.py startapp gs_scheduler 2.2、修改配置文件s…

Typora+PicGo+阿里云OSS搭建个人博客图床(2024最新详细搭建教程)

创作者&#xff1a;Code_流苏(CSDN) 目录 一、什么是图床&#xff1f;二、准备工作三、配置PicGo四、配置Typora五、使用 很高兴你打开了这篇博客&#xff0c;如有疑问&#xff0c;欢迎评论。 更多好用的软件工具&#xff0c;请关注我&#xff0c;订阅专栏《实用软件与高效工具…

基于肤色模型的人脸识别FPGA实现,包含tb测试文件和MATLAB辅助验证

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 matlab2022a的测试结果如下&#xff1a; vivado2019.2的仿真结果如下&#xff1a; 将数据导入到matlab中&#xff0c; 系统的RTL结构图如下图所示…

安装“STM32F4 Discovery Board Programming with Embedded Coder”MATLAB获取硬件支持包失败

安装“STM32F4 Discovery Board Programming with Embedded Coder”MATLAB获取硬件支持包失败 -完美解决方法 显示请续订您的软件维护服务&#xff0c;解决办法 根据知乎的文章 MATLAB获取硬件支持包失败&#xff0c;显示请续订您的软件维护服务&#xff0c;解决办法&#xff…

为家庭公网IP配置DDNS域名

文章目录 域名配置域名更新frp配置修改 在成功完成frp改造Windows笔记本实现家庭版免费内网穿透之后&#xff0c;某天我突然发现内网穿透失效了&#xff0c;一番排查之后原来是路由器对应的公网IP更换了。果然我分到的并不是固定的公网IP&#xff0c;而是会定期变化的。为了免受…

头歌:SparkSQL简单使用

第1关&#xff1a;SparkSQL初识 任务描述 本关任务&#xff1a;编写一个sparksql基础程序。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1. 什么是SparkSQL 2. 什么是SparkSession。 什么是SparkSQL Spark SQL是用来操作结构化和半结构化数据的接口。…

【深耕 Python】Data Science with Python 数据科学(18)Scikit-learn机器学习(三)

写在前面 关于数据科学环境的建立&#xff0c;可以参考我的博客&#xff1a; 【深耕 Python】Data Science with Python 数据科学&#xff08;1&#xff09;环境搭建 往期数据科学博文一览&#xff1a; 【深耕 Python】Data Science with Python 数据科学&#xff08;2&…

2024五一杯数学建模C题思路分享 - 煤矿深部开采冲击地压危险预测

文章目录 1 赛题选题分析 2 解题思路2.1 问题重述2.2 第一问完整思路2.2 二、三问思路更新 3 最新思路更新 1 赛题 C题 煤矿深部开采冲击地压危险预测 煤炭是中国的主要能源和重要的工业原料。然而&#xff0c;随着开采深度的增加&#xff0c;地应力增大&#xff0c;井下煤岩动…

搜索引擎的设计与实现参考论文(论文 + 源码)

【免费】搜索引擎的设计与实现.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89249705?spm1001.2014.3001.5501 搜索引擎的设计与实现 摘要&#xff1a; 我们处在一个大数据的时代&#xff0c;伴随着网络信息资源的庞大&#xff0c;人们越来越多地注重怎样才能…

汽车车灯的材料是什么?汽车车灯的灯罩如果破损破裂破洞了要怎么修复?

汽车车灯的材料主要包括灯罩和灯底座两部分&#xff0c;它们所使用的材料各不相同。 车灯罩的材料主要是透明且具有良好耐热性和耐紫外线性能的塑料。其中&#xff0c;聚碳酸酯&#xff08;PC&#xff09;是一种常用的材料&#xff0c;它具有高抗冲击性、耐化学品腐蚀和优良的…

Pandas入门篇(二)-------Dataframe篇4(进阶)(Dataframe的进阶用法)(机器学习前置技术栈)

目录 概述一、复合索引&#xff08;一&#xff09;创建具有复合索引的 DataFrame1. 使用 set_index 方法&#xff1a;2.在创建 DataFrame 时直接指定索引&#xff1a; &#xff08;二&#xff09;使用复合索引进行数据选择和切片&#xff08;三&#xff09;重置索引&#xff08…

Spring Cloud Kubernetes 本地开发环境调试

一、Spring Cloud Kubernetes 本地开发环境调试 上面文章使用 Spring Cloud Kubernetes 在 k8s 环境中实现了服务注册发现、服务动态配置&#xff0c;但是需要放在 k8s 环境中才能正常使用&#xff0c;在本地开发环境中可能没有 k8s 环境&#xff0c;如何本地开发调试呢&#…

1. 深度学习笔记--神经网络中常见的激活函数

1. 介绍 每个激活函数的输入都是一个数字&#xff0c;然后对其进行某种固定的数学操作。激活函数给神经元引入了非线性因素&#xff0c;如果不用激活函数的话&#xff0c;无论神经网络有多少层&#xff0c;输出都是输入的线性组合。激活函数的意义在于它能够引入非线性特性&am…

小程序wx.getlocation接口如何开通?

小程序地理位置接口有什么功能&#xff1f; 随着小程序生态的发展&#xff0c;越来越多的小程序开发者会通过官方提供的自带接口来给用户提供便捷的服务。但是当涉及到地理位置接口时&#xff0c;却经常遇到申请驳回的问题&#xff0c;反复修改也无法通过&#xff0c;给的理由…

计算机网络chapter1——家庭作业

文章目录 复习题1.1节&#xff08;1&#xff09; “主机”和“端系统”之间有何不同&#xff1f;列举几种不同类型的端系统。web服务器是一种端系统吗&#xff1f;&#xff08;2&#xff09;协议一词常用来用来描述外交关系&#xff0c;维基百科是如何描述外交关系的&#xff1…