【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;
}

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

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

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

相关文章

Redis集群(主从)

1.主从集群 集群结构: 一.单机安装redis 1.上传压缩包并解压&#xff0c;编译 tar -xzf redis-6.2.4.tar.gz cd redis-6.2.4 make && make install 2.修改redis.config的配置并启动redis # 绑定地址&#xff0c;默认是127.0.0.1&#xff0c;会导致只能在本地访问。…

SpringBoot源码解读与原理分析(四十)基于jar/war包的运行机制

文章目录 前言第14章 运行SpringBoot应用14.1 部署打包的两种方式14.1.1 以可独立运行jar包的方式14.1.2 以war包的方式 14.2 基于jar包的独立运行机制14.2.1 可独立运行jar包的相关知识14.2.2 SpringBoot的可独立运行jar包结构14.2.3 JarLauncher的设计及工作原理14.2.3.1 Jar…

2核4G云服务器租用价格_2核4G云主机优惠价格_2024年报价

租用2核4G服务器费用价格&#xff0c;2核4G云服务器多少钱一年&#xff1f;1个月费用多少&#xff1f;阿里云2核4G服务器30元3个月、轻量应用服务器2核4G4M带宽165元一年、企业用户2核4G5M带宽199元一年&#xff1b;腾讯云轻量2核4G服务器5M带宽165元一年、252元15个月、540元三…

毕业生信息招聘平台|基于springboot+ Mysql+Java的毕业生信息招聘平台设计与实现(源码+数据库+文档+PPT)

目录 论文参考 摘 要 数据库设计 系统详细设计 文末获取源码联系 论文参考 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 毕业生信息招聘平台&#xff0c;主要的模块包括查看管理员&a…

力扣经典题目解析--最小覆盖子串

原题地址: . - 力扣&#xff08;LeetCode&#xff09; 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串&#xff0c;则返回空字符串 "" 。 注意&#xff1a; 对于 t 中重复字符&#xff0c;我们寻找…

Finetuning Large Language Models: Sharon Zhou

Finetuning Large Language Models 课程地址&#xff1a;https://www.deeplearning.ai/short-courses/finetuning-large-language-models/ 本文是学习笔记。 Goal&#xff1a; Learn the fundamentals of finetuning a large language model (LLM). Understand how finetu…

Vue3:用vite创建Vue3项目

一、简介 vite是新一代前端构建工具&#xff0c;官网地址&#xff1a;https://vitejs.cn vite的优势如下&#xff1a; 轻量快速的热重载&#xff08;HMR&#xff09;&#xff0c;能实现极速的服务启动。对 TypeScript、JSX、CSS 等支持开箱即用。真正的按需编译&#xff0c;不…

【计算机那些事】

目录 【云计算】 【原神用的是UDP还是TCP】 【几个特殊地址】 【socket是什么】 【内网穿透是什么】 【为什么有HTTP协议&#xff0c;还要有websocket协议】 【科普路由器&#xff0c;集线器&#xff0c;交换机&#xff0c;网桥&#xff0c;光猫】 【USB接口那些事】 …

MacOS包管理工具homebrew使用教程

MacOS包管理工具homebrew使用教程 1.概述与安装2.基本使用3.其他常用命令 1.概述与安装 homebrew是Mac OS X上的强大的包管理工具&#xff0c;可以高效管理各种软件包 安装&#xff1a; 1、安装xcode&#xff1a; xcode-select --install2、一行命令下载&#xff1a; /bin…

个人项目介绍3:火车站篇

项目需求&#xff1a; 一比一精确显示火车站主建筑和站台模型。实时响应车辆信息&#xff08;上水&#xff0c;吸污&#xff0c;换乘&#xff09;并同步显示&#xff0c;实时响应车辆进出站信息&#xff0c;并以动画形式模拟。实时响应报警信息&#xff0c;并能在三位中显示&a…

快速搭建Vue前端框架

快速搭建Vue前端框架 安装Vue Vue官方安装过程:https://cli.vuejs.org/zh/guide/installation.html 二.创建Vue工程 2.2 安装淘宝镜像 安装淘宝镜像&#xff08;会让你安装Vue的速度加快&#xff09;&#xff1a; npm config set registry https://registry.npm.taobao.or…

【内推】金山办公 2024届 春季校园招聘

有需要内推的小伙伴吗&#xff1f; 金山办公 各岗位均有 面向应届生春招 QQ群&#xff1a;723529936 内推码&#xff1a;NTASYQI

十秒学会Ubuntu命令行:从入门到进阶

一、引言 在使用Ubuntu操作系统时&#xff0c;命令行界面&#xff08;CLI&#xff09;是不可或缺的一部分。对于初学者来说&#xff0c;掌握基本的命令行操作可以帮助他们更高效地管理系统和软件。本文将介绍一些常见的Ubuntu命令以及如何解决与命令行相关的问题。 二、常用Ubu…

【C语言】内存操作篇---动态内存管理----malloc,realloc,calloc和free的用法【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】内存操作篇---动态内存管理----malloc&#xff0c;realloc&#xff0c;calloc和free的用法【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 在学完结构体后&#xff08;…

本地搭建xss平台并获取cookie演练

前言 一般而言&#xff0c;搭建xss平台是不被允许的&#xff0c;但是由于教育的目的&#xff0c;搭建xss平台更能让学习者更加直观感受xss漏洞对我们的危害和它的重要性。 搭建xss平台 1.搭建xss平台的基础是在phpstudy一个集成环境上的&#xff0c;所有第一步要安装phpstudy&a…

ardupilot 及PX4姿态误差计算算法对比分析

目录 文章目录 目录摘要1.APM姿态误差计算算法2.PX4姿态误差计算算法3.结论摘要 本节主要记录ardupilot 及PX4姿态误差计算算法差异对比过程,欢迎批评指正。 备注: 1.创作不易,有问题急时反馈 2.需要理解四元物理含义、叉乘及点乘含义、方向余弦矩阵含义、四元数乘法物理含…

Linux环境基础开发工具使用

目录 1.Linux软件包管理器yum 什么是软件包 关于 lrzsz 查看软件包 2.Linux开发工具 2.1.vim的基本概念 2.2vim的基本操作 2.3vim命令模式命令集 1.插入模式 2.从插入模式切换为命令模式 3.移动光标 4.删除文字 5.复制 6.替换 7.撤销上一次的操作 8.更改 2.4v…

Windows10笔记本亮度调节按键失灵

操作&#xff1a;任务管理器 -> 监视器 -> 右键点击 -> 通用即插即用监视器 -> 更新驱动程序 -> 浏览我的电脑以查找我的驱动程序 -> 让我从计算机上的可用驱动程序列表中选取 -> 点击通用即插即用监视器 -> 点击关闭 -> 重启电脑。 第一步&#x…

第三百八十二回

文章目录 1. 概念介绍2. 使用方法3. 代码与效果3.1 示例代码3.2 运行效果 4. 内容总结 我们在上一章回中介绍了"如何修改按钮的形状"相关的内容&#xff0c;本章回中将介绍NavigationBar组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…

波奇学Linux:进程通信之命名管道

进程通信的前提&#xff1a;让不同的进程看到同一份文件 匿名管道只能具有血缘关系的进程&#xff0c;毫不相关的进程通信得要命名管道 管道文件不需要刷盘&#xff0c;基于内存级文件 命名管道通过路径文件名确定打开同一个文件&#xff0c;在匿名管道中利用父子进程。 创…