目录

前言

一、线程ID及进程地址空间布局

二、线程栈与线程局部存储

三、线程封装

总结:


前言

我们在上篇文章着重给大家说了一下线程的控制的有关知识。

但是如果我们要使用线程,就得那这pthread_create接口直接用吗?这样岂不是太过麻烦,要知道,C++,java等语言其实都对这个线程进行了封装,形成了独属于自己语言风格的线程。

今天,我们不仅要来给大家补充一些知识,还会给大家模拟实现一下一个简单的线程封装,希望能够帮助大家更好的学习线程。

一、线程ID及进程地址空间布局

我们之前使用pthread_create的时候曾经提到了线程ID。

我们知道,Linux中没有真正意义上的线程,只有轻量级进程。

每一个线程的数据结构其实都是PCB,所以针对每一个PCB,每一个线程(轻量级进程时调度的最小单位),都会有一个对应的ID来表示该线程,这个ID跟我们学习进程时的pid_t差不多。

但是实际上,pthread_create函数在使用时会产生一个线程ID,并将其存放在第一个参数指向的内存位置。pthread_create返回的线程ID实际上是NPTL线程库在用户空间分配的一个内存地址,这个地址指向线程控制块(TCB),作为线程库内部管理线程的标识符。线程库的后续操作,都是根据这个线程ID来操作的。

线程库提供了pthread_self函数,可以获得线程自身的ID:

pthread_t到底是什么类型呢?这取决与实现。
对于Linux目前实现的NPTL实现而言,pthread_t类型的现场ID,本质上就是一个进程地址空间的地址。
#define _GNU_SOURCE
#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>void* thread_func(void* arg) 
{// 获取当前线程的 pthread_tpthread_t self_id = pthread_self();// 打印 pthread_t 的值(以指针和整数形式)printf("Thread ID (pthread_self):\n");printf("  As pointer: %p\n", (void*)self_id);printf("  As unsigned long: %lu\n", (unsigned long)self_id);return NULL;
}int main() 
{pthread_t tid;// 创建线程pthread_create(&tid, NULL, thread_func, NULL);// 打印主线程看到的 pthread_tstd::cout<<tid<<std::endl;printf("Main thread sees new thread ID:\n");printf("  As pointer: %p\n", (void*)tid);printf("  As unsigned long: %lu\n", (unsigned long)tid);pthread_join(tid, NULL);return 0;
}

可以看出,实质上就是一个地址


我们之前学过一点库。可以知道,我们是先将pthread库加载到物理内存中,通过映射,让自己被看见(共享):

所以库也是共享的,那如果有一百个线程,库的内部岂不是要维护一百份线程的属性集合?库要不要对线程属性进行管理?

:要,怎么管理?:先描述,再组织。

所以有一个结构体,叫做TCB。 这就跟你去图书馆查阅资料一样,图书馆的书都是共享的,但是你需要读者借阅卡。

可以这样理解Linux线程的管理机制:主线程的进程控制块(PCB)通过mmap区域维护着与线程库(libpthread.so)的映射关系,而线程库内部使用一个称为TCB(线程控制块)的关键数据结构来管理线程资源。

每个TCB不仅保存着对应线程的pthread_t标识符(实际上就是TCB自身的地址指针),还记录了该线程独立分配的栈空间信息,包括栈的起始虚拟地址和结束虚拟地址。这些TCB通过链表等形式组织起来,使得线程库能够高效地管理所有线程的私有数据和执行上下文,而内核则只需关注轻量级进程(LWP)的调度,实现了用户态和内核态的协同分工。

每一个线程的TCB,在他创建时就已经在当前进程的堆空间上分配好空间了。

二、线程栈与线程局部存储

刚刚说每个TCB中都记录了当前线程独立分配的栈空间。

没错,每个线程也会独立分配栈空间信息,那么线程的栈与进程的栈有什么区别呢? 

线程栈进程栈(主线程栈)
由线程库(NPTL)通过 mmap 动态分配由内核在进程启动时静态分配
默认大小:8MB(可通过 pthread_attr_setstacksize 调整)

默认大小:8MB(受 ulimit -s 限制)

但是二者最大的区别,就是线程的栈,满了之后是不会自动拓展的。但是进程的栈,是可能会自动拓展的。

子线程的栈原则上是他私有的,但是同一个进程的所有线程生成时,会浅拷贝生成这的task_struct的很多字段,所以如果愿意。其他线程是可以访问到别人的栈区的。这一点我们在上一篇文章也提到过了。


我们之前说过,全局变量在多线程中是共享的,如果你改变了,我看见的也会改变。那有没有什么办法让这个各自私有一份呢?

有的,就是线程局部存储。

我们要用到__thread关键字。

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>__thread int counter = 0;  // 每个线程有独立副本void* thread_func(void* arg) 
{for (int i = 0; i < 3; i++) {counter++;  // 修改线程私有变量printf("Thread %ld: counter = %d (地址: %p)\n",(long)arg, counter, &counter);}return NULL;
}int main() 
{pthread_t t1, t2;pthread_create(&t1, NULL, thread_func, (void*)1);pthread_create(&t2, NULL, thread_func, (void*)2);pthread_join(t1, NULL);pthread_join(t2, NULL);printf("主线程: counter = %d\n", counter);  // 输出0(主线程的独立副本)return 0;
}

在全局变量前使用该关键字,可在各线程中私有一份,这个线程独立存储是在TCB中记录的。


三、线程封装

补充完了线程的知识,接下来我们就进行封装一下我们的线程,方便后续课程的使用。

首先,我们要明确要实现的功能,

封装几个最常用的功能:

  • start():开新线程让它跑起来

  • join():等这个线程干完活

  • detach():让线程自己玩去,不用管它死活

  • stop():强行让线程下岗(这个要小心用)

为了实现这些功能,我们就得想要哪些成员变量帮助我们实现方法,或者记录一下信息:

  • 线程ID(_tid):不然找不到这个线程

  • 线程名(_name):调试时候好认人

  • 能不能join(_joinable):防止重复join搞出事情

  • 进程ID(_pid):这个可能有用先留着


所以我们可以先这样写:

#ifndef _MYTHREAD_HPP_
#define _MYTHREAD_HPP_#include<iostream>
#include<string>namespace ThreadModule
{class Mythread{public:Mythread(){}void start()//负责线程的创建{}void join()//负责线程的等待{}void stop()//负责线程的取消{}~Mythread(){}void detach()//负责线程的分离{}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable;//判断状态,我们之前讲了进程分离};
}#endif

为了后文我们方便调用测试方法,所以我们可以用function,来包含我们的方法(打印之类的),我们规定这个方法就是void(void)的函数,所以我们可以在类成员变量中新加一个方法,为了方便,可以使用重命名:using func_t = std::function<void()>;

另外,我们可以定义一个enum,来定义宏状态来代表线程的运行状态(不是分离):新建,运行,暂停

    using func_t = std::function<void()>;enum class TSTATUS{NEW,RUNNING,STOP};

想完这些,就是来实现我们的函数接口了。

首先是初始化,我们规定我们的线程要传入相应的执行方法,所以构造函数需要外部传入func_t类型。同时,为了方便从名称看出来线程的数量等信息,我们可以在作用域中定义一个static int的number变量来记录,在_name初始化时可以用上。

         Mythread(func_t func):_func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}

然后,就是创建进程,这里我们要注意,先检查状态是否为Running,如果是,就没必要新建一个线程,

这里我们要注意的是,我们需要写一个回调函数Routine,方便我们执行传进来的函数func,以及改变运行状态等操作,为了安全,这个回调函数应该写在private中:

值得注意的是,我们Routine的前缀类型如果没有加static,在我们start中pthread_create时会报错。

因为我们的Routine是类成员函数,真正的函数参数中是有一个this指针的,所以我们这里必须加static限制。

     private:static void *Routine(void*args){Mythread *t = static_cast<Mythread *>(args);t->_status = TSTATUS::RUNNING;t->_func();return nullptr;}
         bool start()//负责线程的创建{if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}

顺便,修改一下start函数返回类型为bool,为了方便我们获取是否成功的信息。(这里是一切从简了,否则我们还可以定义一个返回值错误的enum)


之后,就是对join,stop的封装,实际上底层就是调用我们之前说过的pthread_cancel与pthread_join,所以这里不再过多赘述。

        bool join()//负责线程的等待{if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool stop()//负责线程的取消{if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}

最后,就是线程的分离,我们要判断我们的成员变量_joinable的状态,是否可以进行分离,随后调用分离函数,最后再更新状态:

        bool detach()//负责线程的分离{if (_joinable){int n = ::pthread_detach(_tid);if (n != 0)return false;_joinable = false;}}

为了方便我们后续的打印测试,所以我们可以新加一个name接口返回该线程的名字。

所以我们初代版本的简单线程封装,就已经完成了:

#ifndef _MYTHREAD_HPP_
#define _MYTHREAD_HPP_#include<iostream>
#include<string>
#include<functional>
#include<unistd.h>
#include<sys/types.h>namespace ThreadModule
{using func_t = std::function<void()>;static int number =1;enum class TSTATUS{NEW,RUNNING,STOP};class Mythread{private:static void *Routine(void*args){Mythread *t = static_cast<Mythread *>(args);t->_status = TSTATUS::RUNNING;t->_func();return nullptr;}public:Mythread(func_t func):_func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool start()//负责线程的创建{if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool join()//负责线程的等待{if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool stop()//负责线程的取消{if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}~Mythread(){}bool detach()//负责线程的分离{if (_joinable){int n = ::pthread_detach(_tid);if (n != 0)return false;_joinable = false;}}std::string Name(){return _name;}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable;//判断状态,我们之前讲了进程分离func_t _func;TSTATUS _status;};
}#endif

我们可以写一些代码来测试一下:
 

#include <stdio.h>
#include<iostream>
#include <pthread.h>
#include <unistd.h>#include "Mythread.hpp"int main()
{ThreadModule::Mythread t([](){while(true){std::cout << "hello world" << std::endl;sleep(1);}});t.start();std::cout << t.Name() << "is running" << std::endl;sleep(5);t.stop();std::cout << "Stop thread : " << t.Name()<< std::endl;sleep(1);t.join();std::cout << "Join thread : " << t.Name()<< std::endl;return 0;
}

那么如果我要用多线程呢?

我们这里不使用C++的方法可变参数,我们可以使用我们的老朋友容器来进行管理:
 


using thread_ptr_t = std::shared_ptr<ThreadModule::Mythread>;int main()
{std::unordered_map<std::string, thread_ptr_t> threads;// 如果我要创建多线程呢???for (int i = 0; i < 10; i++){thread_ptr_t t = std::make_shared<ThreadModule::Mythread>([](){while(true){//std::cout << "hello world" << std::endl;sleep(1);}});threads[t->Name()] = t;}for(auto &thread:threads){thread.second->start();std::cout<<thread.second->Name()<<"is started"<<std::endl;}sleep(5);for(auto &thread:threads){thread.second->stop();std::cout<<thread.second->Name()<<"is stopped"<<std::endl;}for(auto &thread:threads){thread.second->join();std::cout<<thread.second->Name()<<"is joined"<<std::endl;}return 0;
}

至此,一旦有了线程对象后,我们就能使用容器的方式对线程进行管理了,所以这就是:先描述再组织。

总结:

我们线程部分的第一阶段的内容就到此结束了,接下来带大家进入二阶段:同步异步等概念知识的学习,届时,我们就会接触到锁等概念了。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【物理与机器学习】从非平衡热力学到扩散模型

[toc] 0.引子:从非平衡热力学开始 1.架构简介 2.反向过程的具体推导与 DDPM 改进摘要&#xff1a;扩散模型将非平衡热力学的“噪声注入—去噪逆转”理念注入生成建模中。DDPM&#xff08;Denoising Diffusion Probabilistic Models&#xff09;在 SD2015 的基础上&#xff0c;通…

Git常用命令详解:从入门到精通

前言 Git作为当今最流行的分布式版本控制系统&#xff0c;已经成为开发者必备的技能之一。无论你是独立开发者还是团队协作&#xff0c;掌握Git的基本操作都能极大提高工作效率。本文将详细介绍Git的常用命令&#xff0c;帮助你快速上手并精通Git的基本使用。 一、Git基础概念…

Vue-22-通过flask接口提供的数据使用plotly.js绘图(一)

文章目录 1 任务背景 2 Flask提供接口(server.py) 2.1 原始代码 2.2 跨域问题 3 Vue3获取数据并渲染Plotly图表 3.1 新建工程 3.2 程序 3.2.1 index.html(入口) 3.2.2 cpmponents/Plot.vue(子组件) 3.2.3 App.vue(父组件) 3.2.4 main.ts 3.3 展示 4 选择图表类型绘图 4.1 App.v…

【mysql】换主键

需求&#xff1a;曲库表的主键错了&#xff0c;原先设置的是(sang_id),应该设置为&#xff08;sang_name,singer&#xff09;联合主键。-- &#xff08;0&#xff09;先备份数据&#xff0c;我这里没备份 -- &#xff08;1&#xff09;进行主键的切换之前&#xff0c;要进行一些…

Redis原理之缓存

上篇文章&#xff1a; Redis原理之集群https://blog.csdn.net/sniper_fandc/article/details/149141342?fromshareblogdetail&sharetypeblogdetail&sharerId149141342&sharereferPC&sharesourcesniper_fandc&sharefromfrom_link 目录 1 Redis作为MySQL…

关于集合的底层数据结构

单列集合Collection分为list集合和set集合list集合分为ArrayList和LinkedListArrayList--底层数据结构是数组1.通过索引查询快2.增删要重构索引,增删慢 LinkedList--底层数据结构是链表1.无索引查询慢2.通过改变前节点的尾指针和后节点的前指针指向可快速增删,增删快set集合(…

批量插入技巧:减少事务提交次数的性能提升

一、事务提交成本分析每次事务提交触发‌磁盘I/O同步‌&#xff08;WAL机制&#xff09;、‌日志写入‌和‌锁资源释放‌操作&#xff0c;高频独立提交会产生指数级开销‌。实验表明&#xff1a;MySQL提交1万次单条插入比单次批量插入‌慢20倍以上‌‌。高频提交还加剧锁竞争与…

importlib.import_module() 的用法与实战案例

&#x1f31f; 一、什么是 importlib&#xff1f; importlib 是 Python 的一个内置标准库&#xff0c;用于在程序运行时 动态导入模块。 &#x1f524; 对比&#xff1a;普通 import vs importlib方式示例特点静态导入import os编写代码时就确定要导入的模块动态导入importlib.…

Oracle 12c 创建数据库初级教程

1. 连接到Oracle sqlplus / as sysdba Oracle数据库名称默认为ORCL或sqlplus /ORCL as sysdba Oracle数据库名称默认为ORCL2. 创建表空间&#xff08;数据库&#xff09; create user YOUR_USERNAME identified by "YOUR_PASSWORD"; YOUR_USERNAME为数据库名称和登…

zabbix服务器告警处理

zabbix服务器告警&#xff0c;信息为&#xff1a;Utilization of poller processes over 75%处理办法为修改zabbix_server.conf配置文件&#xff0c;一般情况下为/etc/zabbix目录下。根据自己轮询器的类型修改对应的轮询器的数量&#xff1b;我这里把StartPollers&#xff0c;S…

随笔20250721 PostgreSQL实体类生成器

我来帮你创建一个C#程序&#xff0c;从PostgreSQL数据库读取表结构并生成对应的实体类文件。我已经创建了一个完整的PostgreSQL实体类生成器。这个程序包含以下主要功能&#xff1a;主要特性数据库连接: 使用Npgsql连接PostgreSQL数据库表结构读取: 自动读取所有表的结构信息类…

B树、B-树与B+树

B树、B-tree与B树 在计算机科学&#xff0c;尤其是数据库和文件系统的领域中&#xff0c;B树、B-tree和B树是理解数据如何被高效存储和检索的关键。它们之间关系紧密&#xff0c;但功能和应用上又存在着决定性的差异。 一、 核心概念澄清&#xff1a;B树就是B-tree 首先需要明确…

视频格式转换工厂v3.2.5,集音视频、图片处理78MB

今天&#xff0c;我们要介绍的是一款功能强大的视频处理软件——视频格式转换工厂。这款软件已经完美破解&#xff0c;无需登录即可享受全部高级功能。它不仅支持视频格式转换&#xff0c;还涵盖了音频、图片处理等多种功能&#xff0c;是一款真正的多媒体处理工具。 视频格式转…

VUE 中父级组件使用JSON.stringify 序列化子组件传递循环引用错误

背景 VUE 中父级组件使用JSON.stringify 序列化子组件传递的数据会报错 runtime-core.esm-bundler.js:268 Uncaught TypeError: Converting circular structure to JSON –> starting at object with constructor ‘Object’ — property ‘config’ closes the circle 原因…

HTTP,HTTPS

在网络工程师、开发工程师、运维工程师等岗位的面试中&#xff0c;​​HTTP/HTTPS​​ 是高频必考知识点&#xff0c;尤其在前端、后端、测试、DevOps等与网络通信相关的职位中。以下是系统化的核心考点梳理&#xff0c;涵盖基础概念、协议机制、安全特性及应聘高频问题。​​一…

Nginx访问日志分析在云服务器环境的技术实现与案例

在云计算时代&#xff0c;Nginx访问日志分析已成为服务器运维的关键环节。本文将深入解析如何通过日志切割、实时监控和可视化展示三大技术路径&#xff0c;实现云环境下Nginx日志的高效分析。我们将结合具体案例&#xff0c;演示从原始日志到运维决策的完整技术闭环&#xff0…

鸿蒙实现一次上传多张图片

记录初接触鸿蒙&#xff0c;遇到的一个问题&#xff0c;需求是点击一个图片上传的号图&#xff0c;访问本地图片&#xff0c;可以选择多张图片并上传。下面是图片上传后的方法&#xff1a;//选择图片并上传private async showPhotoPicker() {const maxImageCount 3;const rema…

【STM32】CRC 校验函数

先上一下 CRC校验 的源代码&#xff1a; void crc_check(unsigned char *ptr,unsigned int len) //crc为开源函数 {unsigned long wcrc0XFFFF;//预置16位crc寄存器&#xff0c;初值全部为1unsigned char temp;//定义中间变量int i0,j0;//定义计数for(i0;i<len;i)//循环计算每…

【Java】SVN 版本控制软件的快速安装(可视化)

目录 一、SVN 的概述 1.1 SVN 的概念 1.2 SVN 与 Git 的对比 1.3 SVN 软件 二、SVN 的安装 2.1 SVN 的工作流程 2.2 服务器端 SVN 的安装 三、SVN 服务器端的配置 3.1 搭建项目 3.2 权限控制 四、SVN 客户端的配置 4.1 SVN 客户端的下载 4.2 客户端连接 SVN 服务器…

Hadoop安全机制深度剖析:Kerberos认证与HDFS ACL细粒度权限控制

Hadoop安全机制概述在大数据时代&#xff0c;Hadoop作为分布式计算框架的核心组件&#xff0c;其安全性直接关系到企业数据资产的保护。随着数据价值的不断提升&#xff0c;Hadoop安全机制已从早期的"简单信任模式"演进为包含多重防护措施的综合体系&#xff0c;其重…