Linux 多线程(五)用C++语言以面向对象方式封装线程

目录

一、用C++语言以面向对象方式封装线程

Main.cc(雏形)

Makefile

Thread.hpp(雏形)

细节1:

细节2:

细节3:

细节4:

[Version1 :](#Version1 :)

Version2:

Version3:

三个版本的对比:

二、线程同步和互斥铺垫

多线程打印错乱:并发问题的本质与解决

三、总结


一、用C++语言以面向对象方式封装线程

本篇文章我们会用C++语言以面向对象方式封装线程,因为我们已经把线程的整个生命周期的流程都学习了,并且还学了C++中的多线程,那本篇我们就用C++来封装一个线程。

三个文件,依次是线程的声明定义文件,主函数文件,makefile文件。

Main.cc(雏形)

Makefile

Thread.hpp(雏形)

pthread_t 是什么类型?

在 Linux 中,pthread_t 本质是一个无符号整数类型(通常是 unsigned long int 或 unsigned int)。它是用来唯一标识一个线程的 ID。

细节1:

为什么出现了类型不兼容?

C++ 类的非静态成员函数,会隐式携带一个 this 指针作为第一个参数,实际为 void* (Thread::*)(Thread* this, void* arg) , 我们上面就是调用的时候忽略了this 指针

解决方案 :

  1. 把 ThreadRoutine 函数放在类外,但是不太优雅,舍弃。

  2. 给 ThreadRoutine 加 static,此时这个函数就随这个Thread类,不再属于某个具体对象,没有隐藏的 this 指针,签名变为 static void* ThreadRoutine(void* arg),完美匹配 pthread_create 的要求。代价就是因为静态函数无法直接访问类的非静态成员(因为没有 this 指针),所以需要通过 arg 参数传入 this 指针,再强转还原。

我们可以给第四个参数传this指针。


细节2:

这和 ThreadRoutine 函数不矛盾,ThreadRoutine只是让我们的线程以固定历程跑起来。

回调函数:

回调函数是什么?

std::function<void()> 是一个通用函数类型,能存放任何无参、无返回值的可调用对象,让函数像普通变量一样传递、保存、调用。

如何调用 :

最终Loop函数还是在ThreadRoutine里执行的。

还想补充的就是 : self 是栈上的局部变量,它会在自己的生命周期内,把所有任务跑完。它会调用 TORunning () 把线程状态改成 RUNNING 和 调用 _cb () 执行用户传进来的线程任务。也就是说在新线程的栈帧里,self 活着的这段时间,它会把所有逻辑执行完毕。

如果 self 销毁,但任务已经做完,做完的任务也不会消失。即就算 self 生命周期结束了,完成的任务还会在。因为 self->_cb() 是用户的任务(比如 lambda、普通函数、成员函数)。它运行在 新线程的栈,而不是运行在 self 的栈。也就是说 self 只是临时的 "调度者",任务不是它的,任务做完就留在线程里。

我们在主函数中用 new 在堆上创建 Thread 对象(C++ 层 TCB),把 this 指针传给 pthread_create;新线程中,栈上的临时指针 self 还原 this,在自己短暂的生命周期内,调用堆上对象的成员函数,完成线程状态修改和用户任务执行;self 销毁后,堆上的对象、执行完的任务全部保留,线程正常运行,self 只是连接 C 接口和 C++ 对象的临时桥梁。

细节3:

因为新建线程默认继承主线程的进程名(如上图的 thread),在 ps -aL 中主线程和子线程名完全一致,无法区分,调试、日志排查极其困难。因此我们可以通过 pthread_setname_np 给子线程设置唯一名称,方便直观区分主线程和子线程,调试时(gdb)快速定位目标线程日志中打印线程名,精准追踪执行流

pthread_setname_np / pthread_getname_np :

所以我们改一下名字

运行结果:

细节4:

线程分离与自动终止

Version1 :

测试结果一 :

Thread t(Loop) 构造函数中,自动生成线程名 New-Thread-1,初始化 _tid=-1、状态 THR EAD_NEW、_joinable=true,并将 Loop 存入 _cb 回调。t.Start() 调用pthread_create,传入 this 指针,创建内核线程,执行静态入口 ThreadRoutine。ThreadRoutine 中还原 this 指针为 self,调用 TORunning(),将状态改为 THREAD_RUNNING,调用 pthread_setname_np ,将线程名设置为 New-Thread-1,执行 self->_cb(),即用户的 Loop 函数,执行 TOStop(),将状态改为 THREAD_STOP,t.Join() 调用 pthread_join,主线程阻塞,直到子线程 Loop 循环 10 次执行完毕。

测试结果二 :

测试结果三 :

创建多个线程 : 有的可能Join,有的可能Detach,有的可能Stop,那主线程肯定要进行管理

先描述再组织

我们现在已经描述了 怎么组织? 链表/数组/二叉树/队列

组织一 : vector

  1. 线程对象批量创建:emplace_back(Loop) 原地构造 10 个Thread对象,每个对象自动生成唯一线程名(New-Thread-1 ~ New-Thread-10),初始化状态为THREAD_NEW,_joinable=true,并将Loop任务存入_cb回调。
  2. 线程批量启动:遍历vector调用t.Start(),逐个调用pthread_create创建内核线程,执行ThreadRoutine,设置线程名、修改状态为THREAD_RUNNING,执行Loop任务;每启动一个sleep(1),避免线程启动竞争。
  3. 线程运行阶段:主线程sleep(10),10 个子线程并行执行Loop循环,持续打印线程名与计数,右侧ps -aL实时监控到 10 个New-Thread-*子线程 + 1 个主线程thread。
  4. 线程批量取消:10 秒后,遍历vector调用t.Stop(),向每个线程发送pthread_cancel取消请求,终止Loop执行,修改状态为THREAD_STOP。
  5. 线程资源回收:遍历vector调用t.Join(),主线程阻塞等待所有子线程退出,回收线程资源;最后调用t.PrintInfo(),打印每个线程的 ID、名称、最终状态、可 join 状态。

运行结果:

PID 统一为 2350009:所有线程属于同一个进程,主线程CMD为thread,子线程CMD为自定义的New-Thread-*,完美区分 10 个并行子线程。

LWP 递增:每个子线程对应唯一的轻量级进程 ID,验证了 10 个线程成功创建、并行运行。

实时刷新:while :; do ps -aL | head -n1; ... sleep 1; done 每秒刷新,完整展示了线程从启动、运行到取消的全过程。

组织二 : 队列

我们可以用队列组织起来 控制它们的操作 所以我们就相当于在用户层面上写了个线程调度器

Version2:

Version2版本主要是和传参问题有关

我们把原本无参无返回值的std::function<void()>,升级成了带模板参数T的std::function<void(T&)> ,实现了线程任务可以接收自定义参数,线程类通过 _data 成员存储参数,在 ThreadRoutine 中把参数传给回调函数,解决了 "线程任务无法传参" 的问题,让线程类更通用、更灵活

Version3:

版本3把线程类从无参 / 固定参数彻底改造为模板化万能参数,同时新增了线程 + 进程嵌套的高级用法,完美适配任意业务场景。

三个版本的对比:


二、线程同步和互斥铺垫

多线程打印错乱:并发问题的本质与解决

为什么会错乱?

  1. 一切皆文件:Linux 中显示器(终端)对应文件描述符 fd=1(标准输出),是进程内所有线程共享的资源。
  2. 多个线程同时调用 std::cout 打印时,cout 的缓冲区写入不是原子操作,多个线程的输出会穿插、覆盖,导致信息错乱。
  3. 如果多个线程同时修改共享变量(如全局计数器、共享对象),会产生竞态条件,导致数据脏读、脏写,最终数据完全错误。

三、总结

本文介绍了使用C++面向对象方式封装线程的方法。主要内容包括:1) 通过Thread类封装线程生命周期管理,解决C++成员函数与pthread_create的类型兼容问题;2) 实现线程命名、状态管理、回调函数等功能;3) 通过vector/queue组织多线程,实现线程调度;4) 逐步优化线程参数传递机制,支持模板化参数;5) 分析多线程并发问题,如输出错乱和共享资源竞争。文章展示了从基础线程封装到高级线程管理的完整演进过程,为多线程编程提供了实用的面向对象解决方案。

谢谢大家的观看!

相关推荐
喵叔哟2 小时前
31_Document处理通用Skill:多格式抽取+结构化输出+校验层
数据库
香蕉鼠片2 小时前
TCP确认应答,超时重传,滑动窗口,流量控制,拥塞控制,延迟应答,捎带应答
服务器·网络协议·tcp/ip
Y淑滢潇潇2 小时前
HCIP IP-VLAN 实验报告
运维·网络·tcp/ip
来鸟 鸣间2 小时前
mutex_lock 流程
linux·c语言
秋风&萧瑟2 小时前
【Linux系统编程】system函数和exec函数族的使用
linux·运维·服务器
搜佛说2 小时前
03-第3章-基础CRUD操作
数据库·物联网·边缘计算·iot·嵌入式实时数据库
秋风&萧瑟2 小时前
【Linux系统编程】Linux多进程介绍及使用
linux·运维·网络
三万棵雪松2 小时前
【Linux 物联网网关主控系统-Web部分(四)】
linux·前端·物联网·嵌入式linux
宵时待雨2 小时前
linux笔记归纳1:linux初识
linux·运维·笔记