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

news2024/10/21 10:20:24

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

        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/2213850.html

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

相关文章

Gin框架官方文档详解03:HTML渲染

官方文档&#xff1a;https://gin-gonic.com/zh-cn/docs/ 注&#xff1a;强烈建议没用过Gin的读者先阅读第一节&#xff1a;第一个Gin应用。 目录 一、简单渲染二、使用不同目录下名称相同的模板三、自定义模板渲染器四、自定义分隔符五、自定义模板函数六、总结 一、简单渲染 …

数通--5

一、链路聚合 背景&#xff1a;带宽不够&#xff0c;加线&#xff0c;但是stp判断是环路&#xff0c;阻塞&#xff0c;等于没加线。通过链路聚合解决&#xff08;如果我把千兆换成万兆&#xff0c;老子有钱任性的话&#xff0c;没得说&#xff09; 现在我要的不是备份而是带宽…

电能表预付费系统-标准传输规范(STS)(4)

5.4 TokenCarrier 到 MeterApplicationProcess 的数据流 The flow of data from the TokenCarrier to the MeterApplicationProcess is shown in Figure 4.此数据流见图 4。 图 4 TokenCarrier 到 MeterApplicationProcess 的数据 The token entry process from the TokenCarr…

抢单超卖? 并发问题解决思路

1. 问题介绍 在用户抢单或者商品售卖的过程中&#xff0c;正常情况下是一人一件&#xff0c;但是当网络流量剧增时多个用户同时抢到一个商品应该如何分配&#xff1f;假设这样一个场景A商品库存是100个&#xff0c;但是秒杀的过程中&#xff0c;一共卖出去500个A商品。对于卖家…

AXI GPIO按键控制——ZYNQ学习笔记4

一、AXI GPIO接口简介 是什么&#xff1f;是PL部分的一个IP软核&#xff0c;实现通用输入输出接口的功能&#xff0c;并通过AXI协议实现与处理系统通信&#xff0c;方便控制与拓展GPIO接口。 AXI GPIO IP 核为 AXI 接口提供了一个通用的输入/输出接口。 与 PS 端的 GPIO 不同&…

【YOLO系列】YOLO11原理和深入解析——待完善

文章目录 前言一、主要新增特性二、主要改进2.1 C3K2网络结构2.2 C2PSA网络结构2.3 Head部分 三、对比与性能优势四、X-AnyLabeling4.1 目标检测&#xff1a;4.2 实例分割&#xff1a;4.3 图像分类&#xff1a;4.4 姿态估计&#xff1a;4.5 旋转目标检测&#xff1a; 五、总结 …

Vue+Vant实现7天日历展示,并在切换日期时实时变换

效果图&#xff1a; 主要使用 moment.js 插件完成 HTML部分 <div class"day-content"><div class"day-content-t"><div>{{ monthVal }}</div><div click"onCalendar()">更多>></div></div><…

HTTP vs WebSocket

本文将对比介绍HTTP 和 WebSocket &#xff01; 相关文章&#xff1a; 1.HTTP 详解 2.WebSocket 详解 一、HTTP&#xff1a;请求/响应的主流协议 HTTP&#xff08;超文本传输协议&#xff09;是用于发送和接收网页数据的标准协议。它最早于1991年由Tim Berners-Lee提出来&…

【C++】二叉搜索树的概念与实现

目录 二叉搜索树 概念 key类型 概念 代码实现 key_value类型 概念 代码实现 二叉搜索树 概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树: 左子树的值默认小于根节点&#xff0c;右子树的值默认大于根节点 。 ⼆…

具备技术三:通用类型any实现

一、背景 一个连接必须拥有请求接收与解析的上下文。 上下文的结构不能固定&#xff0c;因为服务器支持的协议很多&#xff0c;不同协议有不同的上下文结构&#xff0c;所以必须拥有一个容器保存不同的类型结构数据。 二、设计思路 目标&#xff1a;一个容器保存各种不同数…

opencv学习:CascadeClassifier和detectMultiScale算法进行人脸识别

CascadeClassifier CascadeClassifier 是 OpenCV 提供的一个用于对象检测的类&#xff0c;它基于Haar特征和AdaBoost算法。它能够识别图像中的特定对象&#xff0c;比如人脸、眼睛、微笑等。CascadeClassifier 需要一个预训练的XML分类器文件&#xff0c;该文件包含了用于检测…

SHA1算法学习

SHA-1&#xff08;安全哈希算法1&#xff09;是一种加密哈希函数&#xff0c;它接受一个输入并生成一个160位&#xff08;20字节&#xff09;的哈希值&#xff0c;通常表示为一个40位的十六进制数。 SHA1的特点 输入与输出&#xff1a;SHA-1可以接受几乎任意大小的输入&#…

21世纪20年代最伟大的情侣:泰勒斯威夫特和特拉维斯凯尔西每张照片都在秀恩爱

在时代的长河中&#xff0c;每一代都毫无例外地拥有属于自己的 it couple&#xff08;当红情侣&#xff09;&#xff0c;他们成为了那个特定时期大众瞩目的焦点和津津乐道的话题。 千禧年间&#xff0c;确实涌现出了诸多令人瞩目的情侣组合。就像汤姆克鲁斯和凯蒂霍尔姆斯&…

【H2O2|全栈】更多关于HTML(2)HTML5新增内容

目录 HTML5新特性 前言 准备工作 语义化标签 概念 新内容 案例 多媒体标签 音频标签audio 视频标签 video 新增部分input表单属性 预告和回顾 后话 HTML5新特性 前言 本系列博客是对入门专栏的HTML知识的补充&#xff0c;并伴随一些补充案例。 这一期主要介绍H…

从源码上剖析AQS的方方面面(超详细版)

AQS在 ReentrantLock 的使用方式&#xff08;非公平锁&#xff09; 我们之前学习过 ReentrantLock 非公平锁与公平锁的区别在于&#xff0c;非公平锁不会强行按照任务等待队列去等待任务&#xff0c;而是在获取锁的时候先去尝试使用 CAS 改变一下 State&#xff0c;如果改变成…

架构设计笔记-18-安全架构设计理论与实践

知识要点 常见的安全威胁&#xff1a; 信息泄露&#xff1a;信息被泄露或透露给某个非授权的实体。破坏信息的完整性&#xff1a;数据被非授权地进行增删、修改或破坏而受到损失。拒绝服务&#xff1a;对信息或其他资源的合法访问被无条件地阻止。攻击者向服务器发送大量垃圾…

多选框的单选操作 Element ui

文章目录 样式预览Q&#xff1a;为什么要这么做&#xff1f;实现原理探索路程 样式预览 Q&#xff1a;为什么要这么做&#xff1f; 单选框的样式不够好看单选框因为框架等原因&#xff0c;无法取消选择 实现原理 判断多选框绑定的 value&#xff0c;如果长度为2&#xff0c;那…

实缴新玩法:公司注册资金与知识产权的完美结合

在当今商业环境中&#xff0c;公司注册资金的实缴方式不断创新和发展。其中&#xff0c;将公司注册资金与知识产权相结合&#xff0c;成为了一种引人注目的新玩法。 以往&#xff0c;公司注册资金的实缴往往依赖于货币资金的注入。然而&#xff0c;随着知识经济的崛起&#xf…

中文学术期刊(普刊)-全学科

文章目录 一、征稿简介二、重要信息三、服务简述四、投稿须知五、联系咨询 一、征稿简介 二、重要信息 期刊官网&#xff1a;https://ais.cn/u/3eEJNv 三、服务简述 中国知网是最负盛名的中文数据图书馆&#xff0c;收录来自自然科学、社会科学的优质学术期刊&#xff1b;维…

Redis哨兵TILT模式问题解决方案

Redis sentinel的TILT影响范围 Redis版本影响范围&#xff1a;5、6、7版本 部署方式为k8s部署&#xff0c;都会受到影响&#xff0c;裸金属部署没有问题 当redis哨兵集群进入TILT模式后&#xff0c;业务无法正常连接到redis集群&#xff0c;无法正常使用redis集群。 TILT 模式&…