linux线程 | 线程的控制(上)

        前言:本节内容为线程的控制。在本篇文章中, 博主不仅将会带友友们认识接口, 使用接口。 而且也会剖析底层,带领友友们理解线程的底层原理。 相信友友们学完本节内容, 一定会对线程的控制有一个很好的把握。 那么, 现在开始学习吧!

        ps:本节设计线程的概念和进程的知识, 建议友友们了解相关知识点后再来观看!  

目录

pthread库

线程控制

线程创建    

pthread_create接口

实验

全局变量与多线程

多线程发生异常

​编辑 tid

线程等待

pthread_join接口

retval

线程的终止

pthread_exit接口

pthread_cancel接口

使用自定义类作为线程接收对象

线程库的底层原理 

线程栈


pthread库

        我们知道,  所有的线程, 都属于同一个进程所有的线程让他们去打印PID, 那么打印出来的PID最终会是同一个PID。 但是, 我们的线程如果被调度, 那么他就要有一个供别人调用的属于自己的ID。

        那么内核当中,有没有很明确的线程的概念呢? 没有, 内核当中只有一个“轻量级进程”的概念。 但是, 并不影响我们的每一个执行流(线程) 都有属于自己的ID, 这个ID叫做tid。 

        那么, 既然内核中只有“轻量级进程”的概念, 那么他是不是就不会给我们提供线程的系统调用, 只会给我们提供轻量级进程的系统调用!——但是我们用户要使用线程的创建方法, 所以linux程序员, 就在系统和用户层之间开发出了一个pthread线程库。 这个库是在应用层的。是对轻量级进程的相关接口进行封装, 为用户提供直接控制线程的接口。几乎所有的linux平台, 都是默认自带pthread库的!linux中编写多线程代码, 需要使用pthread库!

线程控制

线程创建    

pthread_create接口

        第一个参数是输出型参数, 线程的id。 第二个参数是线程的属性, 一般情况下我们设置称为nullptr就行了。 第三个参数是一个函数指针, 返回值是void*, 参数也是一个void*, 这个函数指针是一个回调函数。 第四个参数, 就是创建线程成功的时候需要参数。 这个参数就是给线程函数传递的。 返回值0表示成功, 非零表示错误。

实验

        下面我们使用一下这个接口, 创建一个新线程。 

#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;


void* threadRoutine(void* args)
{
    while (true)
    {
        cout << "new thread, pid: " << getpid() << endl; 
        sleep(2); 
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);  
    
    while(true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

        那么既然创建的新线程去执行了新的函数, 那么就注定了这两个线程一个线程执行main函数, 访问的是main函数的代码;另一个线程执行threadRoutine函数, 访问的是threadRoutine函数的代码。 然后我们运行结果如下:

        上面这是链接式报错。 这是因为我们这里用的接口不是系统调用, 是库方法!!不是c/c++库, 是第三方库。 我们在学习动态库的时候学过, 这里必须我们自己指定链接哪一个库。 那么链接哪一个库呢? 起始man手册里面已经告诉我们了, 如下图:

        就是链接这个-pthread库。 我们在makefile中, 链接上这个库:

        然后就能编译成功了:

        运行后, 我们可以看到打印出来的PID是一样的:

        同时, 我们如果使用ps axj也能看到进程也只是一个:

        我们想要查到两个执行流, 怎么做呢? 这里有一个选项叫做ps -aL(a表示所有, L可以理解成light)

        图中我们画的这个LWP是什么东西呢? 其实, 在我们的linux中, 并没有真正意义上的线程, 是使用的进程模拟的线程。 cpu调度的时候, 不仅仅只看我们的PID, 每一个轻量级进程也有一个自己的标识符, 就是这个LWP(light weight process id)。所以, cpu在调度进程的时候根部看的不是PID, 看的是LWP!!!

        但是, 我们可以看到, 这个执行流里面, 有一个执行流的PID和LWP是相同的!!!这是怎么回事? ——PID和LWP相同, 就意味着这个进程叫做主线程。 剩下PID和LWP不相等的是被创建出来的线程。

        另外, 我们发送信号, 如果是给一个线程发, 那么同一个进程内的其他线程同样会挂掉。 就如同下图:

全局变量与多线程

        我们创建一个全局变量g_val, 让g_val在主线程进行加加操作。 然后主线程和父线程都对这个g_val进行打印。代码如下:



#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

int g_val = 100;

void* threadRoutine(void* args)
{
    while (true)
    {
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);

        sleep(1); 


    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);  

    while(true)
    {
        printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);

        sleep(1);
        g_val++;

    }


    return 0;
}

运行后我们就会发现g_val会逐渐增大!!同时, 新线程也能够看到这个值会变化。 也就是说, 全局变量, 对于所有的线程来说是可见的!

多线程发生异常

        如果多线程发生了异常, 不管是哪一个执行流发生异常, 都会导致进程退出。 下面是测试代码(五秒后新线程发生除零错误, 异常, 进程退出):


#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

int g_val = 100;



void* threadRoutine(void* args)
{
    while (true)
    {
        printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);

        sleep(5); 

        int a = 10;
        a /= 0;

        // cout << "new thread, pid: " << getpid() << endl; 
        // show("[new thread]");
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);  

    while(true)
    {
        printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        // cout << "main thread, pid: " << getpid() << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
        // show("[main thread]");
        sleep(1);
        g_val++;

    }


    return 0;
}

运行结果: 

 tid

        在上面的实验里面, 我们没有打印过tid。 我们现在以十六进制打印一下这个tid。

        然后运行就能看到显然我们的tid和LWP是不一样的。 

         这是因为LWP是操作系统层面的概念, 作为操作系统自己知道即可, 我们用户并不关心LWP, 我们只关心tid。而这个tid是什么, 其实就是共享区的一块地址(涉及到了底层原理, 后面会讲到)

线程等待

pthread_join接口

        我们先考虑这样一个问题, 对于一个新线程和一个主线程来说, 是主线程先退出, 还是新线程先退出? 如果是进程, 我们知道, 大部分情况下是子进程先退出,因为如果父进程先退出, 新线程就会一直僵尸, 造成内存泄漏。 其实, 我们的线程也是一样的, 线程退出后也需要被等待, 如果不等待, 就会造成类似于僵尸进程的问题。(但是我们使用ps -aL是观察不到的) 

而且我们等待线程和等待进程的目的类似, 有两个:

  •         防止新线程造成内存泄漏。
  •         如果需要, 获取新线程的退出结果。

那么, 等待函数如何使用, 我们使用man手册:

        这里的第一个参数就是被等待线程的tid。第二个参数是线程的返回值。 返回值是int类型, 成功返回零, 失败返回错误码。(注:我们用的这套线程的接口, 它的函数, 几乎都是以pthread开头。 线程类函数里面, 所有的出错码不用errno, 统一使用返回值的方式进行返回。)

retval

        现在我们讨论一下这个第二个参数, 这个第二个参数其实就是拿到我们的新线程的返回值。 我们如果仔细观察就会发现, 我们的的我们的新线程是以回调函数的方式传给pthread_create的, 所以我们无法获得新线程的返回值。 而如果想要拿到这个返回值, 就要用到等待线程时的第二个参数, 这个参数的原理是什么? 下面讲解一下:

        首先, 我们的线程的返回值是在pthread_create里面的。 也就是说,我们的线程的返回值, 返回到了pathread库的接口里面。 那么pthread同时又有一个接口, 叫做phtead_join, 这个接口的第二个参数是一个输出型参数, 它可以拿到phtread_create里面的对应线程的返回值。 所以, 我们想要获得这个返回值, 就要先在外部定义一个void* 类型的接收变量, 然后将这个变量的地址传给pthread_join的第二个参数, 就相当于将等待函数内部的想要给我们带出来的值给我们带出来。 而phtread_join想要给我们带出来的是什么? 就是这个新线程的返回值!!!所以, 我们就能拿到新线程的返回值了。

线程的终止

pthread_exit接口

        首先我们需要知道的是, exit是用来终止进程的, 不能用来终止线程。 如果使用exit终止线程, 会让我们的整个进程都退出。 

        线程库为我们提供了线程的退出方法:pthread_exit

参数类似于exit里面的参数。 就是退出码。 也类似于返回值。 

下面为简单的代码测试

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstdlib>
using namespace std;

void* threadRuntine(void* args)
{
    string name = static_cast<char*>(args);
    int cnt = 5;
    while (cnt--)
    {
        cout << args << " say#: " << "I am a new thread" << endl;
        sleep(1);
    }
    pthread_exit((void*)1);
}


int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRuntine, (void*)"thread 1");

    void* ret;
    pthread_join(tid, &ret);
    cout << (long long int)ret << endl;
    return 0;
}

pthread_cancel接口

        这个函数的作用是给新线程发送一个取消请求, 并且退出的线程, 退出码为-1。(注意, 不常用)

void* threadRuntine(void* args)
{
    string name = static_cast<char*>(args);
    int cnt = 5;
    while (cnt--)
    {
        cout << args << " say#: " << "I am a new thread" << endl;
        sleep(1);
    }
    pthread_exit((void*)1);
}


int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRuntine, (void*)"thread 1");

//两秒后就直接退出
    sleep(2);
    pthread_cancel(tid);

//
    void* ret;
    pthread_join(tid, &ret);
    cout << (long long int)ret << endl;
    return 0;
}

使用自定义类作为线程接收对象

我们可以使用自定义类型作为线程的接收对象。

        我们使用一下自定义类型的对象, 实验过程为: 首先定义两个类, 一个Request, 一个Response。 其中Request的成员变量有一个start, 一个end。 我们新线程就是来计算从start到end的总和。 然后就是Response, Response用来新线程的返回。里面的成员包含一个_result, 一个_exitcode。 


#include<iostream>
#include<unistd.h>
#include<string>
#include<cstdlib>
#include<pthread.h>
using namespace std;


class Request
{
public:
    Request(int start, int end, string threadname)
        :_start(start)
        ,_end(end)
        ,_threadname(threadname)
    {}

public:
    int _start;
    int _end;
    string _threadname;
};


class Response
{
public:
    Response(int result, int exitcode)
        :_result(result)
        ,_exitcode(exitcode)
    {}

public:
    int _result;
    int _exitcode;
};

void* threadRuntine(void* args)
{
    Response* rsp = new Response(0, 0);
    Request* req = static_cast<Request*>(args);
    for (int i = req->_start; i <= req->_end; i++)
    {
        rsp->_result += i;
        usleep(100000);
        cout << ".exe is running..." << i << endl;
    }
    return (void*)rsp;
}

int main()
{
    Request* req = new Request(1, 100, "thread 1");
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRuntine, (void*)req);

    void* ret;

    pthread_join(tid, &ret);
    cout << "result: " << static_cast<Response*>(ret)->_result << endl;

    return 0;
}

运行结果:

线程库的底层原理 

        我们理解线程库的大概底层原理, 其实只要理解下面博主画的这张图即可:

        就是说, 我们的pthread库是在用户层的。 然后呢, 我们的线程库里面,就保存着我们的线程要执行的方法, 以及独立栈。 什么意思, 这里的方法其实就是我们pthread_create里面的第二个参数, 它是由我们的线程库所维护的。 我们的线程库维护执行方法以及独立栈, 然后将他们作为第一, 第二参数传给系统调用clone。 然后这个执行方法要暴露给我们的用户, 让我们的用户来定义轻量级进程要执行的代码。 所以, 线程的概念是由库给我们维护的(线程在底层对应的轻量级进程的执行流。 但是我们线程中很多用户关心的字段, 属性由库来维护,比如独立栈, 比如运行的代码。)。 所以, 当我们执行自己写的多线程的代码的时候, 我们的库要不要加载到内存中? ——一定要的, 因为我们的pthread是一个动态库, 所以pthread一定要先加载到内存中,然后通过页表共享, 映射到我们进程地址空间的共享区。

        我们上面说了, 线程库要维护线程的各种属性。 那么我们的线程这么多, 线程库要维护他们。 注意, 维护, 其实本质就是管理。 所以, 我们的线程库势必要先描述, 再组织。 所以, 在线程库当中, 每创建一个线程, 就要创建一个对应的线程控制块。 这里面有很多字段, 比如独立栈在哪里, 回调函数在哪里, 线程id是什么, LWP指向底层的哪一个执行流。 并且, 未来, 我们的用户使用这个线程控制块, 系统就能直接访问下层的之心六, 执行对应的代码, 所以, 这个也叫做用户级线程!!

线程栈

        我们再来谈一下线程栈:就是, 每一个线程在创建的时候, 都有一个独立的栈结构。 这是因为每个线程都有一个自己的调用链。 也就注定了每一个线程都要有自己独立的调用链所对应的栈帧结构。 这个栈结构里面会保存任何执行流在运行过程中的所有的临时变量, 所以, 每一个线程都要有自己的独立的栈结构。 

        其中, 主线程直接用我们的地址空间里面提供的栈结构即可, 其他的都是用线程库里面提供的独立栈结构, 大概的做法就是首先在库里面为新线程创建要给描述线程的线程控制块。 这个线程控制块的起始地址就是自己的线程的tid。 这个里面有一块默认大小的空间, 这个空间就叫做线程栈。 然后就要在内核中创建执行流了, 就是在库里面调用clone, 然后把线程执行的方法, 以及刚刚创建的线程栈作为第一,二参数传递给clone。 所以, 除了主线程, 所有其他线程的独立栈, 都在共享区。 具体来讲是在pthread库中,tid指向的用户tcb中!!

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!  

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

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

相关文章

Spring AI 整体介绍_关键组件快速入门_prompt_embedding等

Spring AI&#xff1a;Java开发者的AI集成新利器 在过去&#xff0c;Java开发者在构建AI应用时面临着缺乏统一框架的问题&#xff0c;导致不同AI服务的集成过程复杂且耗时。Spring AI应运而生&#xff0c;旨在为基于Java的应用程序提供一个标准化、高效且易于使用的AI开发平台…

用PHP爬虫API数据获取商品SKU信息实战指南

在电子商务的精细化运营中&#xff0c;SKU&#xff08;Stock Keeping Unit&#xff0c;库存单位&#xff09;信息是商品管理的核心。它不仅包含了商品的规格、价格、库存等关键数据&#xff0c;还直接影响到库存管理、价格策略和市场分析等多个方面。本文将介绍如何使用PHP爬虫…

Java程序设计:spring boot(3)——spring boot核心配置

目录 1 设置 Banner 图标 1.1 Banner 图标⾃定义 1.2 Banner 图标关闭 2 Spring Boot 配置⽂件 3 Starter 坐标 & ⾃动化配置 3.1 Starter坐标配置 3.1.1 Web starter 3.1.2 Freemarker Starter & Thymeleaf starter 3.1.3 JavaMail邮件发送 Starter 3.1.4 引…

mysql--表的约束

目录 理解表的约束和操作 如何理解&#xff1f; 1、空属性null 2、默认值default 3、列描述comment 4、自动填充zorefill 5、主键primary key &#xff08;1&#xff09;创建表时指定可以 &#xff08;2&#xff09;创建表后指定key &#xff08;3&#xff09;删除主…

注册函数和回调函数使用讲解

1.概念 注册和回调函数在C语言编程中非常常见&#xff0c;也经常用到。注册和回调的机制也大量使用在Linux内核中。学会使用注册和回调函数是C语言开发者应当掌握的一项编程技能。 函数的本质在内存上体现的是地址。我们知道函数的地址后&#xff0c;就能够调用这个函数。 …

ESP32移植Openharmony外设篇(1)MQ-2烟雾传感器

外设篇 实验箱介绍 旗舰版实验箱由2部分组成&#xff1a;鸿蒙外设模块&#xff08;支持同时8个工作&#xff09;、鸿蒙平板。 其中&#xff0c;鸿蒙平板默认采用RK3566方案。 OpenHarmony外设模块采用底板传感器拓展板方式&#xff0c;底板默认采用ESP32方案&#xff0c;也…

部署Qwen2.5-7b大模型详解

部署Qwen2.5-7b大模型详解 本文参考教程&#xff1a;https://qwen.readthedocs.io/en/latest/getting_started/quickstart.html 下载模型 https://modelscope.cn/organization/qwen 搜索 qwen2.5-7b 可以看到它提供了六个模型&#xff0c;以满足不同的需求&#xff0c;从下…

HBuilder X中搭建Vue-cli项目组件和路由以及UI库使用(二)

一、创建组件 &#xff08;1&#xff09;在vj1项目src|右键|vue文件 &#xff08;2&#xff09;组件常用模版 <!--该标签用于写HTML代码,必须有一个根标签,如下<div>是根标签--> <template> <div>首页</div> </template><!--该标签用…

c++算法第3天

本篇文章包含三道算法题&#xff0c;难度由浅入深&#xff0c;适合新手练习哟 目录 第一题 题目链接 题目解析 代码原理 代码编写 本题总结 第二题 题目链接 题目解析 代码原理 代码编写 第三题 题目链接 题目解析 代码原理 代码编写 第一题 题目链接 [NOIP2…

【word】页眉横线无法取消

小伙伴们日常想在页眉里加横线&#xff0c;直接双击页眉&#xff0c;然后在页眉横线里选择自己喜欢的横线样式就可以了。 但今天我遇到的这个比较奇特&#xff0c;有些页有这个横线&#xff0c;有些页没有&#xff0c;就很奇怪。 最后排查完&#xff0c;发现是只有标题2的页…

拓数派创始人冯雷出席联合国人居署《未来城市顾问展望2024》 报告结题专家会

近日&#xff0c;联合国人居署中国未来城市顾问委员会在内蒙古鄂尔多斯市国际会展中心召开《未来城市顾问展望2024&#xff1a;数字城市治理》报告结题会暨走进鄂尔多斯市活动。拓数派创始人、董事长兼首席执行官冯雷&#xff08;Ray Von&#xff09;应邀出席本次活动&#xff…

《计算机视觉》—— 疲劳检测

文章目录 一、疲劳检测实现的思想二、代码实现 一、疲劳检测实现的思想 了解以下几篇文章有助于了解疲劳检测的方法 基于dlib库的人脸检测 https://blog.csdn.net/weixin_73504499/article/details/142977202?spm1001.2014.3001.5501 基于dlib库的人脸关键点定位 https://blo…

个人博客搭建 | Hexo框架

文章目录 1.Hexo安装2.创建博客3.将博客通过GitHub来部署4.更换主题 1.Hexo安装 Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown&#xff08;或其他标记语言&#xff09;解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。搭建Hexo首先要…

基于vue框架的的大连金州红星社区物业管理系统dg6co(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;楼栋信息,住户,社区投诉,设备报修,报修完成,车位信息,缴费信息,房屋信息,维修工,保安,来访人员,缴费申诉,公共设备,设备类型,消防设备,公共场地 开题报告内容 基于Vue框架的大连金州红星社区物业管理系统的设计与实现开题报告 一、研究…

如果使用 Iptables 配置端口转发 ?

现实生活中&#xff0c;港口转发就像在一个大型公寓大楼里告诉送货司机该去哪里。通常情况下&#xff0c;该建筑群的正门是不对外开放的。但如果里面有人想要快递&#xff0c;他们可以告诉保安让司机进来&#xff0c;并指引他们到特定的公寓。 类似地&#xff0c;在计算机网络…

Android复杂问题分析工具bugreportz详解

文章目录 bugreportz详细介绍功能与作用使用方法生成详细报告检查进度bugreportz 的优势分析报告 如何分析1. 解压 ZIP 文件2. 分析主要文件2.1 bugreport.txt2.2 logcat.txt2.3 kernel.log / last_kmsg2.4 events.log2.5 traces.txt2.6 dumpstate_board.txt 3. 工具支持4. 重点…

Axure重要元件三——中继器添加数据

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 本节课&#xff1a;中继器添加数据 课程内容&#xff1a;添加数据项、自动添加序号、自动添加数据汇总 应用场景&#xff1a;表单数据的添加 案例展示&#xff1a; 步骤…

SpringColoud GateWay 核心组件

优质博文&#xff1a;IT-BLOG-CN 【1】Route路由&#xff1a; Gateway的基本构建模块&#xff0c;它由ID、目标URL、断言集合和过滤器集合组成。如果聚合断言结果为真&#xff0c;则匹配到该路由。 Route路由-动态路由实现原理&#xff1a; 配置变化Apollo 服务地址实例变化…

No.17 笔记 | XXE漏洞:XML外部实体注入攻击

1. XXE漏洞概览 XXE&#xff08;XML External Entity&#xff09;是一种允许攻击者干扰应用程序对XML输入处理的漏洞。 1.1 XXE漏洞比喻 想象XML解析器是一个听话的机器人&#xff0c;而XXE就是利用这个机器人的"过分听话"来获取不应该获取的信息。 1.2 XXE漏洞危…

vue综合指南(六)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Vuet篇专栏内容:vue综合指南 目录 101、Vue 框架怎么实现对象和数组的监听&#xff1f; 102、Proxy 与 Object.d…