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

news2024/9/28 1:24:49

文章目录

  • 线程概念
  • 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.coloradmin.cn/o/1486112.html

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

相关文章

【c++】继承深度解剖

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;了解什么事继承&#xff0c;基类和派生类的使用和…

启动CMD/powershell命令窗口时,设置默认的python虚拟环境 in window10

启动CMD/powershell命令窗口时&#xff0c;设置默认的python虚拟环境 in window10 本文有两个目的&#xff1a; CMD命令窗口&#xff0c; 设置默认启动的python环境PowerShell命令窗口&#xff0c; 设置默认启动的python环境 CMD命令窗口&#xff0c; 设置默认启动的python环…

企业计算机服务器中了360勒索病毒如何解密,360后缀勒索病毒处理流程

对于众多的企业来说&#xff0c;企业的数据是企业发展的核心&#xff0c;越来越多的企业开始注重企业的数据安全问题&#xff0c;但随着网络技术的不断发展与应用&#xff0c;网络黑客的攻击加密手段也在不断升级。近期&#xff0c;云天数据恢复中心接到多家企业的求助&#xf…

PDN分析及应用系列二-简单5V电源分配-Altium Designer仿真分析-AD

PDN分析及应用系列二 —— 案例1:简单5V电源分配 预模拟DC网络识别 当最初为PCB设计打开PDN分析仪时,它将尝试根据公共电源网络命名法从设计中识别所有直流电源网络。 正确的DC网络识别对于获得最准确的模拟结果非常重要。 在示例项目中已经识别出主DC网络以简化该过程。 …

人工智能之Tensorflow程序结构

TensorFlow作为分布式机器学习平台&#xff0c;主要架构如下&#xff1a; 网络层&#xff1a;远程过程调用(gRPC)和远程直接数据存取(RDMA)作为网络层&#xff0c;主要负责传递神经网络算法参数。 设备层&#xff1a;CPU、GPU等设备&#xff0c;主要负责神经网络算法中具体的运…

【旧文搬运】为你的 Laravel 应用添加一个基于 Swoole 的 WebSocket 服务

做了一个基于 Swoole 的 WebSocket 扩展包&#xff0c;可以用来做实时状态推送&#xff0c;或者自定义消息处理实现 im&#xff0c;有需要的可以看看: [giorgio-socket] 使用方法 安装 安装扩展包 composer require wu/giorgio-socket发布配置文件 php artisan vendor:pu…

可莉炸鱼

情况有s*k>n&#xff0c;最多炸鱼数为n s*k<n&#xff0c;最多炸鱼数为s*k 将s*k转化为k个s相加&#xff0c;每次结果与n比较 #include<iostream> #include<vector> #include<algorithm> using namespace std; #define endl \n #define ll long lon…

Java---文件,流✨❤️

文章目录 1.遍历文件夹2.遍历子文件夹3.练习流4.以字节流的形式读取文件内容5.以字节流的形式向文件写入数据顶折纠问6 .写入数据到文件 1.遍历文件夹 一般说来操作系统都会安装在C盘&#xff0c;所以会有一个 C:\WINDOWS目录。 遍历这个目录下所有的文件(不用遍历子目录) 找出…

pyqt5怎么返回错误信息给页面(警告窗口)

在软件设计中&#xff0c;我们可能会遇到对异常的处理&#xff0c;有些异常是用户需要看到的&#xff0c;比如说&#xff0c;当我们登录出错的时候&#xff0c;后端需要给我们返回响应的错误信息&#xff0c;就像下图实现的这样。 类似这种效果&#xff0c;我们该如何实现&…

选择何种操作系统作为网站服务器

选择操作系统时&#xff0c;需考虑稳定性、安全性、成本、兼容性和技术支持等因素&#xff0c;常见选项有Windows Server和Linux发行版。 选择网站服务器的操作系统是一个关键的决策&#xff0c;因为它将影响到网站的性能、稳定性、安全性以及未来的扩展性&#xff0c;目前市场…

ES向量功能实战:向量搜索

1 缘起 项目需要&#xff0c;向量搜索使用ES&#xff0c;为了顺利使项目顺利交付&#xff0c;开始学习ES的稠密向量功能&#xff0c;本文即ES向量的实践&#xff1a;增删查改。ES从7.x版本支持向量功能&#xff0c;为测试ES向量功能&#xff0c;需要使用7.x及以上的版本。本文…

算法刷题day20:二分

目录 引言概念一、借教室二、分巧克力三、管道四、技能升级五、冶炼金属六、数的范围七、最佳牛围栏 引言 这几天一直在做二分的题&#xff0c;都是上了难度的题目&#xff0c;本来以为自己的二分水平已经非常熟悉了&#xff0c;没想到还是糊涂了一两天才重新想清楚&#xff0…

vue3 vite项目一运行就401(Unauthorized)

问题&#xff1a;项目一执行&#xff1a; pnpm run dev, 启动就出错&#xff0c; Failed to load resource: the server responded with a status of 401 (Unauthorized) 分析&#xff1a; 项目之前是正常运行的&#xff0c;没有问题&#xff0c;回溯刚刚改动&#xff0c;还原…

W5300驱动说明

W5300是一款带有硬件协议栈的网络芯片&#xff0c;内部拥有128K的缓存&#xff0c;最大支持8路socket通信&#xff0c;与MCU之间通过16位数据总线通信&#xff0c;通信速度远超W5500之类以SPI作为通信接口的网络芯片&#xff0c;特别适合对高速网络传输有需求的应用。 本次使用…

基于springboot+vue的疫苗发布和接种预约系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

关于工业 24V 电源转换电路参考设计

一. 概述 在工业邻域的应用中&#xff0c;部分电路板输入电源为 24V&#xff0c;而电路板上 MCU 及外设等的供电多为 3.3V、5V 以及 12V&#xff0c;因此设计将 24V 降压转换为各种电压非常有必要。常用的电源转换芯片有 DCDC 及 LDO 等&#xff0c;了解选型依据及其电路…

DolphinScheduler——调度系统数仓任务编排规范

原文大佬的这篇DS数仓任务编排规范有借鉴意义&#xff0c;这里摘抄下来用作学习和知识沉淀。 前言 在使用DolphinScheduler&#xff08;以下简称DS&#xff09;做数仓任务管理时&#xff0c;数据建模分层落地到调度上缺少规范&#xff0c;往往比较随意&#xff0c;例如将所有任…

OpenCV 4基础篇| OpenCV图像的裁切

目录 1. Numpy切片1.1 注意事项1.2 代码示例 2. cv2.selectROI()2.1 语法结构2.2 注意事项2.3 代码示例 3. Pillow.crop3.1 语法结构3.2 注意事项3.3 代码示例 4. 扩展示例&#xff1a;单张大图裁切成多张小图5. 总结 1. Numpy切片 语法结构&#xff1a; retval img[y:yh, x…

【JavaEE进阶】CSS选择器的常见用法

CSS选择器的主要功能就是选中页面指定的标签元素&#xff0c;选中了元素&#xff0c;才可以设置元素的属性。 CSS选择器主要有以下几种: 标签选择器类选择器id选择器复合选择器通配符选择器 接下来用代码来学习这几个选择器的使用。 <!DOCTYPE html> <html lang&q…

【C++】set、multiset与map、multimap的使用

目录 一、关联式容器二、键值对三、树形结构的关联式容器3.1 set3.1.1 模板参数列表3.1.2 构造3.1.3 迭代器3.1.4 容量3.1.5 修改操作 3.2 multiset3.3 map3.3.1 模板参数列表3.3.2 构造3.3.3 迭代器3.3.4 容量3.3.5 修改操作3.3.6 operator[] 3.4 multimap 一、关联式容器 谈…