linux线程 | 线程的控制

news2024/10/11 17:32:31

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

        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中!!

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

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

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

相关文章

算法:反转链表

一、题目描述 给定单链表的头节点 head &#xff0c;请反转链表&#xff0c;并返回反转后的链表的头节点。 二、解题思路 1.迭代法 原始链表中&#xff0c;每个结点的 next 指针都指向后一个结点。反转链表之后&#xff0c;每个结点的 next 指针都指向前一个结点。因此&…

Unity UndoRedo(撤销重做)功能

需求 撤销与重做功能 思考 关于记录的数据的两点思考&#xff1a; 记录操作记录影响显示和逻辑的所有数据 很显然这里就要考虑取舍了&#xff1a; 记录操作 这种方案只需要记录每一步的操作&#xff0c;具体这个操作要怎么渲染和实现出来完全需要自己去实现&#xff0c;这…

怎么下载安装yarn

安装 npm install --global yarn 是否安装成功 yarn -v Yarn 淘宝源安装&#xff0c;分别复制粘贴以下代码行到黑窗口运行即可 yarn config set registry https://registry.npm.taobao.org -g yarn config set sass_binary_site http://cdn.npm.taobao.org/di…

免杀对抗—python分离免杀无文件落地图片隐写SOCK管道

前言 之前就基本把所有语言都讲了一遍了&#xff0c;C/C&#xff0c;Java&#xff0c;python&#xff0c;golang&#xff0c;汇编。今天就开始讲免杀的技巧以及手法&#xff0c;分离免杀之前讲过一点&#xff0c;就是通过http或者参数获取shellcode&#xff0c;今天把其他的分…

ppt压缩文件怎么压缩?压缩PPT文件的多种压缩方法

ppt压缩文件怎么压缩&#xff1f;当文件体积过大时&#xff0c;分享和传输就会变得困难。许多电子邮件服务对附件的大小有限制&#xff0c;而在网络环境不佳时&#xff0c;上传和下载大文件可能耗时较长。此外&#xff0c;在不同设备上播放时&#xff0c;较大的PPT文件还可能导…

Chromium HTML attribute与c++接口对应关系分析

<a href"https://www.w3school.com.cn" target"_blank">访问 W3School</a>前端这些属性定义在html_attribute_names.json5文件中&#xff1a; third_party\blink\renderer\core\html\html_attribute_names.json5 html_attribute_names.json5…

【前端碎片记录】大文件分片上传

大文件分片上传&#xff0c;主要是为了提高上传效率&#xff0c;避免网络问题或者其他原因导致整个上传失败。 HTML部分没什么特殊代码&#xff0c;这里只写js代码。用原生js实现&#xff0c;框架中可参考实现 // 获取上传文件的 input框 const ipt document.querySelector(…

Richtek立锜科技线性稳压器 (LDO) 选型

一、什么是LDO? LDO也可称为低压差线性稳压器&#xff0c;适合从较高的输入电压转换成较低输出电压的应用&#xff0c;这种应用的功率消耗通常不是很大&#xff0c;尤其适用于要求低杂讯、低电流和输入、输出电压差很小的应用环境。 二、LDO的特性 LDO透过控制线性区调整管…

【每日一坑】pcb出的光绘文件导入到cam350有两个警告

pcb出的光绘文件导入到cam350有两个警告&#xff1a; 1 Warning - Zero radius arc detected. Assuming linear interpolation. 2 Warning - Apertures are used which have a size of 0. 这个 应该检查到处光绘文件时候&#xff0c;默认的线宽是否为0&#xff1b; 通过负片…

面试八股文对校招的用处有多大?C/C++语言篇

前言 1.本系列面试八股文的题目及答案均来自于网络平台的内容整理&#xff0c;对其进行了归类整理&#xff0c;在格式和内容上或许会存在一定错误&#xff0c;大家自行理解。内容涵盖部分若有侵权部分&#xff0c;请后台联系&#xff0c;及时删除。 2.本系列发布内容分为12篇…

单通道 LVDS 差分线路接收器MS21112S

MS21112S 是一款单通道低压差分信号 (LVDS) 线 路接收器。在输入共模电压范围内&#xff0c;差分接收器可以 将 100mV 的差分输入电压转换成有效的逻辑输出。 该芯片可应用于 100Ω 的受控阻抗介质上&#xff0c;进行点对 点基带数据传输。传输介质可以是印刷电路板、…

图像处理(二)——MDPI特刊推荐

特刊征稿 01 期刊名称&#xff1a; Computer Vision and Image Processing, 2nd Edition 截止时间&#xff1a; 投稿截止日期&#xff1a;2024年12月31日 目标及范围&#xff1a; 感兴趣的主题包括但不限于&#xff1a; 用于图像分类和识别的深度学习 对象检测和跟…

EdgeNAT: 高效边缘检测的 Transformer

EdgeNAT: Transformer for Efficient Edge Detection 介绍了一种名为EdgeNAT的基于Transformer的边缘检测方法。 1. 背景与动机 EdgeNAT预测结果示例。(a, b):来自BSDS500的数据集的输入图像。(c, d):对应的真实标签。(e, f):由EdgeNAT检测到的边缘。(e)显示了由于颜色变化…

QT元对象系统特性详细介绍(信号槽、类型信息、动态设置属性)(注释)

目 录 一、元对象系统简介 二、信号和槽 三、类型信息 四、动态设置属性 一、元对象系统简介 QT中的元对象系统Q_OBJECT并不是C标准代码&#xff0c;因此在使用时需要QT的MOC&#xff08;元对象编译器&#xff09;进行预处理&#xff0c;MOC会在编译时期读取C代码中的特定…

暗语源码 复现佛禅翻译系统v2升级版源码

与佛论禅翻译系统 一个翻译佛论的娱乐系统&#xff0c;类似于核心价值观加密 此为升级版&#xff0c;每次加密得到的结果不一样&#xff0c;配合箴言功能&#xff0c;更加安全 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89874751 更多资源下载&a…

现代易货交易:重塑价值,引领未来交易新风尚

在当今经济蓬勃发展的背景下&#xff0c;一种新颖的交易模式——现代易货交易&#xff0c;正逐渐崭露头角并赢得市场的认可。这一模式不仅对传统物品交换方式进行了革新&#xff0c;更在物品价值的评估与交换手段上展现出创新性。那么&#xff0c;现代易货交易究竟是何方神圣&a…

基于SSM的旅游网站【附源码】

基于SSM的旅游网站&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1 系统概要设计 4.2 系统功能结构设计 4.3 数据库设计 4.3.1 数据库E-R图设计 4.3.2 数据库表结构设计 5 系统实现 5.1 管理员功能介绍 5.1.1 用户管理 5.1.2 …

比较模拟数据

模拟数据检查器可以比较来自工作区、文件或模拟中的运行和单个信号的数据和元数据。可以使用公差来分析比较结果&#xff0c;并可以通过指定信号属性和比较约束来配置比较行为。此示例使用从模型slexAircraftExample的模拟中记录的数据&#xff0c;演示了以下内容&#xff1a; …

云栖实录 | MaxCompute 迈向下一代的智能云数仓

本文根据2024云栖大会实录整理而成&#xff0c;演讲信息如下&#xff1a; 演讲人&#xff1a; 张治国 | 阿里云智能集团研究员、阿里云 MaxCompute 负责人 谢德军&#xff5c;阿里云智能集团资深技术专家 于得水&#xff5c;阿里云智能集团资深技术专家 谌鹏飞&#xff5c…

SpringMVC源码-@ControllerAdvice和 @InitBinder注解源码讲解

1.ControllerAdvice修饰的类何时被加载扫描 被ControllerAdvice修饰的类是作用于全局的 initStrategies 初始化springmvc的9大组件 initStrategies:531, DispatcherServlet (org.springframework.web.servlet) onRefresh:514, DispatcherServlet (org.springframework.web.se…