首页 > 系统相关 >【操作系统---Linux】Linux编程中最常用的控制线程的函数(内附手绘底层逻辑图 通俗易懂)

【操作系统---Linux】Linux编程中最常用的控制线程的函数(内附手绘底层逻辑图 通俗易懂)

时间:2025-01-14 12:57:08浏览次数:3  
标签:函数 内附 void Linux id 线程 pthread tid 逻辑图

绪论​
请添加图片描述
每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”

绪论​:
本章是继承上一章线程基础,本章将结合代码和逻辑图的方式带你去认识和了解控制线程中常用的函数这些函数对后面的开发以及对线程底层的了解都非常的重要,后续将继续更新Linux线程的更多知识,敬请期待吧~
————————
早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。

1. 线程控制的函数:

1.1创建线程:pthread_create:

头文件:
#include <pthread.h>
int pthread_create(pthread_t *thread, 
const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

参数:

  1. thread:线程id(类似于进程的pid,输出型)
  2. attr:线程的属性(暂时不管统一写成nullptr)
  3. start_routine:函数指针,他的类型是void*( * )(void*),代表多线程执行的函数(自己自定义他的任务)
  4. arg:是用来给第三个函数指针传参数的 (因为其类型是void*所以我们可以传递任意类型进去!(包括传递对象))

Linux中本质是没有线程的,只有轻量级进程的概念,所以Linux OS只会提供轻量级进程创建的系统调用,不会直接提供线程的创建接口
在学习操作系统时只学过操作系统中的线程,没有学过Linux中轻量级进程(LWP)概念的人也能正常的使用,就必须得在操作系统和用户之间重新在写一个pthread原生线程库让所有人都能正常使用通过线程,而底层其实是轻量级进程。

既然他是一个第三方库,那在编译时就需要接入外部库,通过-l附加指令(-l库名字)
所以通过编译器g++,需要附加:-lpthread引入线程库
在这里插入图片描述

注:其中如何传参给第三个参数的函数:
这个函数的参数类型是void*,也就表示能接受任何类型,在内部使用时进行强转即可。

练习证明线程的健壮性低

使用上面函数 以及 证明一个线程崩溃会影响所有线程崩溃:
代码:

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

using func_t = function<void()>;//function创建了一个接收函数签名void()的包装器类型,他能接收该类型的函数(void:返回值,():函数没有参数)


//创建一个对象,其中包含了线程名,线程创建的时间,该线程所要执行的函数
class ThreadData
{
public:
    ThreadData(const string& name,const uint64_t & ctime,func_t f)
    :threadname(name),createtime(ctime),func(f)
    {}

public:
    string threadname;//线程名
    uint64_t createtime;//创建时间
    func_t func;//接收函数(void())的包装器
};


void* ThreadRountine(void*arg)//创建一个线程后调用的函数,其中arg是参数,通过第四个参数传递进来
{ 
    ThreadData* td = static_cast<ThreadData*>(arg);//进行类型的强转成,对象
    while(true)
    {
        td->func();//对象中存着,真的调用的函数!

        cout << "threadname: "<< td->threadname << " create time: "<< td->createtime  <<endl;//通过对象访问成员变量
        sleep(1);

        if(td->threadname == "thread-4")//在4号进处出产生信号,观察情况
        {
            cout << td->threadname << " create exeption" << endl;
            int i = 1;
            i /= 0;//让4号线程进来时发生除零异常导致崩溃看看是否会影响 
        }
    }
}

void Print()
{
    cout << "I am Thread of part: ";
}
const int cnt = 5;
int main()
{
    cout << "main thread" << endl;

    for(int i = 0; i < cnt ;i++)//主线程循环多次,创建多个线程
    {
        sleep(1);
        char tname[64];
        snprintf(tname,sizeof(tname),"%s-%d","thread",i);//让每个下标对应一个进程名tname

        ThreadData* td = new ThreadData(tname,(uint64_t)time(nullptr),Print);//创建一个对象,并初始化
        //time函数获取当前时间(传递空)
        pthread_t tid;
        pthread_create(&tid,nullptr,ThreadRountine,td);//创建进程
    }    

    while(true)
    {
        sleep(10);
    }

    return 0;
}

最终结果如下图(4号线程崩溃导致所有线程崩溃):
在这里插入图片描述

1.2 获取线程id:pthread_self

头文件:
#include <pthread.h>
pthread_t pthread_self(void);

实操代码:

using func_t = function<void()>;

string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id,sizeof(id),"0x%lx",tid);
    return id;
}

void* ThreadRountine(void*arg)
{ 
    string threadname = static_cast<const char*>(arg);
    while(true)
    {
        cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
        sleep(1);
    }
}

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

    while(true)
    {
        cout << "main thread , sub thread " << tid << " main thread id: "<< ToHex(pthread_self())<<endl;
        sleep(1);
    }
    return 0;
}

最终结果如下图:
在这里插入图片描述
其中不难发现线程id值非常大,把它通过自定义函数ToHex转成十六进制来看,发现它很像一个地址,那他是地址吗?
在这里插入图片描述
再通过ps -aL查看线程的LWP发现和线程id并不一样?
在这里插入图片描述
上述两问题,我们继续往下看,后面通过底层分析解释(因为内容较多,见2.线程id和LWP到底是什么)。

1.3 线程的终止

1. 默认情况下,线程跑完后就会终止

在这里插入图片描述

string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id,sizeof(id),"0x%x",tid);
    return id;
}

void* ThreadRountine(void*arg)
{ 
    string threadname = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {   
        cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
        sleep(1);
    }
    return nullptr;
}

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

    while(true)
    {
        cout << "main thread , sub thread " << ToHex(tid) << " main thread id: "<< ToHex(pthread_self())<<endl;
        sleep(1);
    }
    return 0;
}

其中注意的是:exit(int)是用来终止的进程(不能作为线程的一种),否则整个进程都会被终止!

2. 用pthread_exit 终止线程

头文件:#include<pthread.h>

void* ThreadRountine(void*arg)
{ 
    string threadname = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {   
        cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
        sleep(1);
    }
    // return nullptr;
    pthread_exit(nullptr);
}

这里替代上面代码中的同位置函数即源码

1.4 线程的返回

  1. 线程返回的时候默认是要被等待的
  2. 进程直接退出,没有等待线程的话,会导致类似进程的僵尸问题

那么就继续引出下面等待函数

1.4.1 线程等待的函数:

头文件:#include <pthread.h>

int pthread_join(pthread_t thread,
 void **retval);
  1. thread:线程的id
  2. retval:一个输出型参数,用于获取线程的返回值(该参数得到线程返回值 void*,外部要得到就得使用void**),也就是等待后其参数二会接收来自对应线程的返回值

返回值:如果等待成功返回0,失败则返回错误码

using func_t = function<void()>;//包装器

string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id,sizeof(id),"0x%x",tid);
    return id;
}

void* ThreadRountine(void*arg)
{ 
    string threadname = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt--)
    {   
        cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
        sleep(1);
    }
    return (void*)"thread-done";//返回的是这个字符串常量的起始地址
    // pthread_exit(nullptr);
}

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


    cout << " main thread id: "<< ToHex(pthread_self())<<endl;

    sleep(8);
    void* ret = nullptr;
    int n = pthread_join(tid,&ret);
    cout << "main thread get new thread return: " << (const char*)ret <<endl;//获取线程的返回值,并打印
    
    return 0;
}

返回用ret接收,因为其类型是void*,进行强转为const char打印出来。
在这里插入图片描述
同理因为返回的是void
所以可以返回的任意类型的(包括对象),外部使用强转回来即可使用。

1.5 线程的状态

  1. joinable状态:线程默认为joinable的主线程会在pthread_join处阻塞式的等待线程
  2. 分离状态:就不用等待了主线程就不用管线程,线程运行结束就自动退出了。(不等待)

状态可以理解成:一个家庭,家庭成员他们相互有关也就是joinable的(你的事情父母会管),而分离状态也就也就相当于儿子和父母的关系不好分家了,他们就互不关心了(资源隔离)。
不过线程再怎么分离,资源还是多线程共用的,一个线程出问题别的线程也会出错。


那么状态的不同就引出了不同的情况,也就引出了不同状态时使用的不同函数


1.5.1 线程分离的函数:

头文件:#include <pthread.h>

int pthread_detach(pthread_t thread);

该函数可以在线程内使用,也可以直接在主线程内使用(参数指定要分离的线程id)。

实操查看线程的两种状态(分离和等待)
void* ThreadRountine(void*arg)
{ 
    pthread_detach(pthread_self());
    int cnt = 5;
    while(cnt--)
    {   
        cout << "new thread runing... "<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()  
{
    pthread_t tid;
    pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
    
    sleep(1);
    int n = pthread_join(tid,nullptr);
    cout << "main thread get new thread return: " << n <<endl;
    
    return 0;
}

其中返回错误码 22 是通用的错误代码,在不同的编程语言、系统或环境中可能有不同的含义。通常,它对应的描述是 Invalid argument(无效参数)。
在这里插入图片描述
通过上图,可以发现打印好像出了点问题,但其实本质就体现了线程的分离状态,线程和主进程分离了,也就形成了异步执行打印的操作,这样才出来点问题。

让我们继续看不分离的状态,也就是joinable状态(默认就是,所以注释分离函数即可)

void* ThreadRountine(void*arg)
{ 
    // pthread_detach(pthread_self());
    int cnt = 5;
    while(cnt--)
    {   
        cout << "new thread runing... "<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()  
{
    pthread_t tid;
    pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
    sleep(1);
    pthread_detach(tid);

    int n = pthread_join(tid,nullptr);
    cout << "main thread get new thread return: " << n <<endl;
    
    return 0;
}

在这里插入图片描述

1.6 取消(终止)线程函数pthread_cancel

头文件:#include <pthread.h>

int pthread_cancel(pthread_t thread);

成功返回0,反之返回错误码

  1. 取消后等待的退出码结果是-1(-1就表明线程是被取消掉的)
    在这里插入图片描述
    线程通过调用pthread_cancel异常终止,retval(pthread_join的第二个参数)所指向的单元里存放的是常量PTHREAD_CANCELED

  2. 线程如果是被分离的,该线程仍可以被pthread_cancel取消,但不能被pthread_join等待。
    在这里插入图片描述
    并且主线程也是能取消的,取消后主线程内的代码将不会被执行。

2. 线程的id和LWP到底是什么?

首先了解线程中的概念:

  1. Linux中所有的线程接口都不是系统直接提供的接口,而是原生线程库pthread提供的接口
  2. 每个操作系统的任意版本都必须默认配备一个该库,否则多线程在LInux下跑步起来。

线程和系统的关系

在这里插入图片描述

  1. 用户成若有5个线程,那么本质上就是在内核系统中就会有5个LWP

  2. Linux的线程一般称为用户级线程(因为本质线程只是在用户层实现,底层是轻量级进程)在这里插入图片描述

  3. 对此所有线程(轻量级进程)都需要被管理,所以在中间的pthread库也需要能管理系统中的LWP(上图)

这样我们就能知道:

其实线程的id本质是原生库中的概念,而LWP是系统底层的概念,在Linux下在内核态和用户态见有一个线程库(他本质是为了给没学过LWP的人也能方便的使用的),其中线程库中有他自己的id(并且线程库是能找到对应线程tcb的),而底层使用的就是LWP,也就是通过id找到对应的tcb(id只是用于找到对应的tcb的他其实用的就是LWP),详细见3.2。

3.底层系统调用细节

线程的独立属性:

  1. 上下文(寄存器中存储)
  2. 栈结构,只有一个栈(只有一套寄存器),但有多个堆

3.1 创建轻量级进程

头文件:
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...);
  1. fn:是所要执行的方法(回调函数)
  2. child_stack:是申请所要使用的栈的空间地址(允许用户传入栈空间)
  3. flag:用来区分创建的是进程还是轻量级进程
  4. arg:是用来给第一个函数传参的

pthread创建线程的本质就是通过函数clone创建的:
也就表示

  1. pthread_create()的底层是封装了clone的
  2. clone它同样也是fork的底层。

在这里插入图片描述
child_stack可以通过malloc来申请空间,所以每个新线程的栈都是在库中维护,其中线程库在内存共享区有指向自己的栈空间的地址变量。而默认地址空间的栈,由主线程使用。


3.2 如何理解pthread库来管理线程

在这里插入图片描述
一个操作系统可能有多个进程,而这些进程他们所使用的都是同一个相同的库,所以称他为共享的!
在这里插入图片描述
并且线程是在用户地址空间也就是共享区(内存映射段,不了解的可以搜一下内存模型,如下图)
在这里插入图片描述
其中mmap区就是在内存映射段的(存储着线程库)

在这里插入图片描述
mmap区域的动态库中就包含有线程库,其中当一个线程的创建就会再动态库中创建一个线程的属性集:
在这里插入图片描述

struct pthread会存着线程的属性:

  1. 线程的内部存储
  2. 线程栈会指向一块自己的空间

线程的创建和管理通常由线程库(如 pthread)抽象化实现,底层依赖于 clone 系统调用。使用 clone 创建线程时,需要提供用户函数(线程执行的函数)、栈地址和一组标志位,这些标志位决定了线程之间共享的资源(如地址空间、文件描述符等)。线程的属性集由线程库维护,底层 clone 系统调用完成轻量级进程的创建。

当线程完成执行时,可以通过返回一个 void* 值向调用方传递结果。通过调用 pthread_join,可以等待线程完成,并获取其返回值。线程库维护一个类似数组的结构,用于存储每个线程的属性集(如线程 ID、栈地址、返回值等)。线程 ID(tid)可作为索引,用于高效管理和调度线程。

每个线程的属性集可以看作数组中的一个元素。线程库在底层以动态数组的形式维护这些属性集,随着线程的创建,不断扩展数组结构,以便支持更多的线程。
具体如下图(类似于数组的底层):
在这里插入图片描述
所以我们所用的线程id,它就是线程属性集合在库中的地址(每个属性集的起始地址)!!!所以pthread_join通过线程id就能很好的拿到想要拿到的线程的数据。

LWP和线程id不一样是因为:LWP是内核的概念,而tid是线程库里的概念(人们不用理解LWP)。


本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量Linux细致内容,早关注不迷路。

标签:函数,内附,void,Linux,id,线程,pthread,tid,逻辑图
From: https://blog.csdn.net/ZYK069/article/details/144943955

相关文章

  • Linux 硬盘扩容 分区 & 挂载
    Linux硬盘扩容分区&挂载1.添加分区1.1.查看新添加的硬盘fdisk-l假设当前未挂载的盘符是/dev/sdb,后文中所有操作都按挂载/dev/sdb操作1.2.分区管理小硬盘fdisk/dev/sdb大硬盘(2TB以上)gdisk/dev/sdb1.3.编辑分区⚠️下方注释一行一行看,不要跳过#执行......
  • 15个Linux Grep命令使用实例(实用、常用)
    Grep命令主要用于从文件中查找指定的字符串。首先建一个demo_file:复制代码代码如下:$catdemo_fileTHISLINEISTHE1STUPPERCASELINEINTHISFILE.thislineisthe1stlowercaselineinthisfile.ThisLineHasAllItsFirstCharacterOfTheWordWithUpper......
  • linux-大数据常用命令
    1.vi/vim一般模式语法 功能描述yy 复制光标当前一行y数字y 复制一段(从第几行到第几行)p 箭头移动到目的行粘贴u 撤销上一步dd 删除光标当前行d数字d 删除光标(含)后多少行x 删除一个字母,相当于delX 删除一个字母,相当于Backspaceyw 复制一个词dw 删除一个词shift+^ 移动到行头shift+$......
  • Linux 运维必备 150 个命令汇总
    地址:https://www.linuxcool.com线上查询及帮助命令man:全拼manual,用来查看系统中自带的各种参考手册。help:用于显示shell内部命令的帮助信息。文件和目录操作命令ls:全拼list,列出目录的内容及其内容属性信息。cd:全拼changedirectory,切换当前......
  • linux-杂项
    1、常用基础防火墙systemctlstatusfirewalldsystemctlstopfirewalldsystemctlstartfirewalldfind/-size+100M-lsnetstat-tunlpiptables-nLnetstat-tulncat/etc/hosts.allowcat/etc/hosts.denytopfree-gcat/etc/passwdcat/etc/hosts.allowcat/etc/hosts.deny......
  • 【linux】文件与目录命令 - vim
    文章目录1.基本用法2.常用参数3.用法举例4.多种模式5.注意事项vim是一款功能强大的文本编辑器,适用于代码编辑和日常文本处理。它是vi的增强版,支持多种模式(如普通模式、插入模式和命令模式)以及插件扩展。1.基本用法语法:vim[选项][文件]功能:编......
  • 超级实用!优化Fiddler抓包默认生成的html报告(内附源码)
    Fiddler实现把抓包结果导出到html报告中之前写了一篇文章关于如何把Fiddler抓包的结果保存到html报告中,具体可以参考文章一文带大家了解如何在Fiddler中生成html测试报告_fiddler导出测试报告-CSDN博客报告整体效果如下所示很显然这个报告的易读性是较差的,最近我把这个......
  • Linux网络编程4——系统编程
    一.电脑知识1.电脑基础CPU:相当于大脑,核心处理器内存条:存放临时数据,相当于人体的临时记忆磁盘:存放长时间数据,相当于人体的长时间记忆显卡:将各种影像输出的装置主板:将所有的零件存储起来2.磁盘磁盘要放入计算机且被Linux系统识别,到可以使用磁盘存储数据,过程如下:1.磁盘......
  • Linux系统中解决端口占用问题
    在日常的Linux系统管理和开发过程中,端口占用是一个常见且令人头疼的问题。无论是部署新服务、调试应用程序,还是进行系统维护,遇到端口被占用都可能导致服务无法正常启动或运行。本文将详细介绍在Linux系统中如何识别和解决端口占用问题,帮助你高效地管理系统资源。一、常见的......
  • Windows 与 Linux 程序员的区别
    如果一个程序员从来没有在Linux,Unix下开发过程序,一直在Windows下面开发程序,同样是工作10年,大部分情况下与在Linux,unix下面开发10年的程序员水平会差别很大。这篇文章并不是想贬低Windows下面开发的人,做Windows开发的人看了可能会感觉不舒服,我并不是这个意思,我只是说说我自己的......