【Linux/C++多线程篇(一) 】多线程编程入门:从核心概念到常用函数详解

【Linux/C++多线程篇(一) 】多线程编程入门:从核心概念到常用函数详解

⭐️在这个怀疑的年代,我们依然需要信仰

个人主页:YYYing.

⭐️Linux/C++进阶系列专栏:【从零开始的linux/c++进阶编程】

系列上期内容:【Linux/C++多进程篇(二) 】linux系统编程之进程间通信 (IPC)

系列下期内容:【Linux/C++多线程篇(二) 】同步互斥机制 & C++ 11下的多线程


目录

前言:为什么需要多线程?

多线程基础概念

一、进程与线程的区别

二、进程与线程的关系

三、多线程的优缺点

 📖 优点

 📖 缺点

多线程编程

一、创建线程:pthread_create

 📖 向线程体中传递单个数据

 📖 向线程体中传入多个数据

二、线程号的获取:pthread_self

三、线程的退出函数:pthread_exit

四、线程的资源回收:pthread_join

五、线程分离态:pthread_detach

 📖 小问题

方法1:使用 pthread_exit()

方法2:让主线程等待(但不阻塞)

六、多线程编程的小练习

结语

---⭐️封面自取⭐️---



前言:为什么需要多线程?

        想象一下,你现在是一个厨师,需要同时做三件事:切菜、煮汤、接电话。如果你只能一件一件地做,顾客会等得不耐烦,汤可能煮干,电话也可能被挂断。在计算机世界里,程序也常常需要同时处理多个任务——比如一边响应用户操作,一边下载文件,一边更新界面。其也是为了实现多任务并发执行的问题的,能够实现多个阻塞任务同时执行

多线程就是让一个程序拥有多个执行流,可以“同时”做多件事。这里的“同时”有两种含义:

  • 并发:指系统能够同时处理多个任务的能力。在单核 CPU 上,通过时间片轮转,宏观上看起来是“同时”运行,微观上是“交替”执行。
  • 并行:指多个任务在同一时刻真正同时运行。这需要多核 CPU 支持,每个核跑一个线程。

多线程的核心价值在于提高程序的响应性、资源利用率和吞吐量

多线程基础概念

一、进程与线程的区别

  • 进程:是资源分配的基本单位,拥有独立的地址空间、文件描述符、堆栈等。进程间切换开销大。
  • 线程:是CPU调度的基本单位,隶属于进程,共享进程的地址空间和大部分资源,拥有独立的栈和寄存器上下文。线程间切换开销小,通信方便。

二、进程与线程的关系

  • 多线程(LWP轻量版进程):线程是粒度更小的任务执行单元
  • 进程是资源分配的基本单位,而线程是任务器进行任务调度的最小单位
  • 一个进程可以拥有多个线程,同一个进程中的多个线程共享进程的资源,而由于线程是共用进程的资源,所以对于线程的切换而言,开销较小,但多个线程使用的是同一个进程的资源,那么就会导致每个进程使用资源时,产生资源抢占问题,没有多进程安全
  • 每个进程至少有一个线程:主线程
  • 只要有一个线程中退出了进程,那么所有的线程也就结束了,主线程结束后,整个进程也就结束了。
  • 多个线程执行顺序:没有先后顺序,按时间片轮询,上下文切换,抢占CPU的方式进行

三、多线程的优缺点

 📖 优点

  • 响应性:即使一个线程阻塞(如等待I/O),其他线程仍可运行。
  • 资源共享:线程间共享内存,无需复杂IPC。
  • 经济性:创建线程比创建进程开销小得多。
  • 可扩展性:可充分利用多核CPU。

 📖 缺点

  • CPU 调度:操作系统内核负责线程调度,上下文切换需要保存/恢复寄存器、更新内存管理结构,这些都需要时间。如果线程数远大于 CPU 核心数,切换开销会占据大量 CPU 时间。
  • 开发复杂度大大增加:多线程引入了竞态条件、死锁等并发问题,这些错误往往难以复现和调试。比如我们曾遇到一个死锁问题,只在生产环境高并发时出现,用 gdb 无法重现,最后靠分析 core dump 和代码走查才定位到。

多线程编程

        我们先讲C语言的多线程编程,至于C++的线程支持库我们后面也会讲。

        再讲之前多提一嘴,由于C库没有提供有关多线程的相关操作,对于多线程编程要依赖于第三方库——头文件:#include<pthread.h>,且编译时:需要加上 -lpthread 选项,链接上对于的线程支持库

一、创建线程:pthread_create

函数原型int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine) (void *), void *arg);
头文件pthread.h
功能创建一个分支线程
参数说明

参数1:线程号,通过参数返回,用法:在外部定义一个该类型的变量,将地址传递入函数,调用结束后,该变量中即是线程号

参数2:线程属性,一般填NULL,让系统使用默认属性创建一个线程

参数3:是一个回调函数,一个函数指针,需要向该参数中传递一个函数名,作为线程体执行函数该函数由用户自己定义,参数是void*类型,返回值也是void *类型

参数4: 是参数3的参数,如果不想向线程体内传递数据,填NULL即可

返回值成功返回0,失败返回一个错误码(非linux内核的错误码,是线程支持库中定义的一个错误码)

        我们现在来实践一下此处的创建线程函数,其中我们线程体传递参数不同数目写法也不一样,现在我们来看看:

 📖 向线程体中传递单个数据

注意:我们传过来的参数必须要进行强转,当然此处若不想添加参数,就直接填NULL即可,但线程体中的 void* arg 仍需要填写

#include<iostream> #include<cstdio> #include<cstring> #include <unistd.h> #include <pthread.h> using namespace std; //定义线程体函数 void *task(void *arg){ //arg --> &num 但是arg是一个void*类型的变量,需要转换为具体指针进行操作 //(int*)arg --->将其转换为整型的指针 //*(int *)arg --->num的值 int key = *(int*)arg; printf("我是分支线程:num = %d\n", key); //1314 } /**************主程序********************************/ int main(int argc, const char *argv[]){ pthread_t tid = -1; //用于存储线程号的变量 int num = 520; if(pthread_create(&tid, NULL, task, &num) != 0){ //参数2:表示让系统使用默认属性创建一个线程 //参数3:线程体函数名 //参数4:表示向线程体中传递的数据 printf("tid create error\n"); return -1; } printf("pthread_create success,tid = %#lx\n", tid); printf("我是主线程\n"); num = 1314; //主线程中更改数据 printf("主线程中num = %d\n", num); while(1); return 0; }

 📖 向线程体中传入多个数据

        可以看到,此处我们若想传递多个数据就必须用到结构体,不能用逗号隔开一个一个传

#include<iostream> #include<cstdio> #include<cstring> #include <unistd.h> #include <pthread.h> using namespace std; //定义信息结构体,用于向线程体传递数据 struct Info{ int num; char name[20]; double score; }; //定义线程体函数 void *task(void *arg){ Info buf = *((Info*)arg); //将结构体指针转换为结构体变量 printf("分支线程中:num = %d, name = %s, score = %.2lf\n", buf.num, buf.name, buf.score); } /**************主程序********************************/ int main(int argc, const char *argv[]) { pthread_t tid = -1; //用于存储线程号的变量 int num = 520; char name[20] = "zhangsan"; double score = 99.5; //需求:将上面的三个数据全部传入线程体中 Info buf = {num, "zhangsan", score}; if(pthread_create(&tid, NULL, task, &buf) != 0){ //参数2:表示让系统使用默认属性创建一个线程 //参数3:线程体函数名 //参数4:表示向线程体中传递的数据 printf("tid create error\n"); return -1; } printf("pthread_create success,tid = %#lx\n", tid); printf("我是主线程\n"); while(1); return 0; }

二、线程号的获取:pthread_self

函数原型pthread_t pthread_self(void);
头文件pthread.h
功能获取当前线程的线程号
参数说明

返回值返回调用线程的id号,不会失败

三、线程的退出函数:pthread_exit

函数原型void pthread_exit(void *retval);
头文件pthread.h
功能退出当前线程
参数说明表示退出时的状态,一般填NULL
返回值

四、线程的资源回收:pthread_join

函数原型int pthread_join(pthread_t thread, void **retval);
头文件pthread.h
功能阻塞回收指定线程的资源
参数说明

参数1:要回收的线程线程号

参数2:线程退出时的状态,一般填NULL

返回值成功返回0,失败返回一个错误码

五、线程分离态:pthread_detach

函数原型int pthread_detach(pthread_t thread);
头文件pthread.h
功能将指定线程设置成分离态,被设置成分离态的线程,退出后,资源由系统自动回收
参数说明要分离的线程号
返回值成功返回0,失败返回一个错误码

        我们不妨更直观的看看四和五的区别:

#include<iostream> #include<cstdio> #include<cstring> #include <unistd.h> #include <pthread.h> //定义线程体函数 void *task(void *arg) { printf("分支线程,tid = %#x\n", pthread_self()); //2、调用函数,输出当前线程的线程号 sleep(3); //3、退出线程 pthread_exit(NULL); } /**********************主程序****************************/ int main() { //1、定义一个线程号变量 pthread_t tid = -1; //创建一个线程 if(pthread_create(&tid, NULL, task, NULL) != 0) { printf("pthread_create error\n"); return -1; } printf("主线程,tid = %#x\n", tid); //4、回收分支线程的资源 //pthread_join(tid, NULL); //前3秒,主线程处于休眠等待分支线程的结束 //分支线程处于休眠状态 //将线程设置成分离态(非阻塞) pthread_detach(tid); sleep(5); // while(1); //防止主线程结束 std::cout << "Hello, World!" << std::endl; return 0; }

 📖 小问题

        那我相信肯定也会有人发现,那分离进程如果没运行完,主程序退出后,那么此时分离进程会被回收吗?

        答案是:会的,但是线程不会有机会执行自己的清理代码。也就是不会自动"回收分离进程",而是整个进程会被强制终止,所有线程(包括分离线程)都会被立即杀死。

  • 主线程退出(从main返回或调用exit()) -> 整个进程终止,所有线程(包括分离线程)立即停止,资源由操作系统回收,但不会执行线程的清理代码。
  • 主线程调用pthread_exit() -> 主线程终止,但进程继续运行,直到所有线程(包括分离线程)结束。这样分离线程可以正常完成并执行清理代码。

        所以面对这种情况,我们有两种解决方式:

方法1:使用 pthread_exit()
int main() { pthread_t tid; pthread_create(&tid, NULL, worker, NULL); pthread_detach(tid); // 只退出主线程,不终止进程 pthread_exit(NULL); // 进程继续,分离线程可以运行完成 // 这里不会执行 return 0; }
方法2:让主线程等待(但不阻塞)
int main() { pthread_t tid; pthread_create(&tid, NULL, worker, NULL); pthread_detach(tid); // 主线程做自己的事,但最后等待一下 // 比如用条件变量或简单的sleep sleep(10); // 等待worker线程大概完成 return 0; }

六、多线程编程的小练习

        我们现在使用多线程完成两个文件的拷贝,线程1拷贝前一半内容,线程2拷贝后一半内容,主线程用于回收两个分支线程的资源

#include<iostream> #include<cstdio> #include<cstring> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/wait.h> #include<pthread.h> //定义要向线程体函数中出入数据的结构体类型 struct Info { const char *srcfile; //要拷贝的原文件 const char *destfile; //目标文件 int start; //起始位置 int len; //要拷贝的长度 }; //定义获取文件长度的函数 int get_file_len(const char *srcfile, const char *destfile) { //定义两个文件描述符,分别作为源文件和目标文件的句柄 int sfd, dfd; //以只读的形式打开源文件 if((sfd = open(srcfile, O_RDONLY)) == -1) { perror("open srcfile error"); return -1; } //以只写的形式打开目标文件 if((dfd = open(destfile, O_RDWR|O_CREAT|O_TRUNC, 0664)) == -1) { perror("open destfile error"); return -1; } //获取源文件的长度 int len = lseek(sfd, 0, SEEK_END); //关闭文件 close(sfd); close(dfd); return len; } //定义线程体函数 void *task(void *arg) { //将传入的数据解析出来 const char *srcfile = ((struct Info*)arg)->srcfile; const char *destfile = ((struct Info*)arg)->destfile; int start = ((struct Info*)arg)->start; int len = ((struct Info*)arg)->len; //准备拷贝工作 //定义两个文件描述符,分别作为源文件和目标文件的句柄 int sfd, dfd; //以只读的形式打开源文件 if((sfd = open(srcfile, O_RDONLY)) == -1) { perror("open srcfile error"); return NULL; } //以只写的形式打开目标文件 if((dfd = open(destfile, O_RDWR)) == -1) { perror("open destfile error"); return NULL; } //偏移指针 lseek(sfd, start, SEEK_SET); lseek(dfd, start, SEEK_SET); //拷贝工作 int ret = 0; //记录每次读取的数据 int count = 0; //记录拷贝的总个数 char buf[128] = ""; //数据搬运工 while(1) { ret = read(sfd, buf, sizeof(buf)); //将读取的数据放入到count中 count += ret; if(count >= len) { //说明该部分的内容拷贝结束,还剩最后一次 write(dfd, buf, ret - (count-len)); break; } //其余的正常拷贝 write(dfd, buf, ret); } //关闭文件描述符 close(dfd); close(sfd); } int main(int argc, const char *argv[]) { //判断传入的文件个数是否正确 if(argc != 3) { printf("input file error\n"); printf("usage:./a.out srcfile destfile\n"); return -1; } //获取原文件的长度,顺便将目标文件创建出来 int len = get_file_len(argv[1], argv[2]); //创建两个线程 pthread_t tid1, tid2; //定义向线程体函数传参的变量 struct Info buf[2] = {{argv[1], argv[2], 0, len/2}, \ {argv[1], argv[2], len/2, len-len/2}}; if(pthread_create(&tid1, NULL, task, &buf[0]) != 0) { printf("线程创建失败\n"); return -1; } if(pthread_create(&tid2, NULL, task, &buf[1]) != 0) { printf("线程创建失败\n"); return -1; } //主线程中完成对两个分支线程资源的回收 pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("拷贝成功\n"); return 0; }

结语

        文介绍了多线程的基础概念和各种函数的基本用法。但这只是冰山一角,真正的挑战在于后续的线程同步(Mutex, Condition Variable)、原子操作(Atomic)以及无锁编程

        在下一篇博客中,我们将深入探讨如何解决竞态条件,揭开互斥锁与同步机制的神秘面纱。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️封面自取⭐️---

Read more

基于模仿学习的机器人操作:分类、演化、基准测试与挑战

基于模仿学习的机器人操作:分类、演化、基准测试与挑战

25年9月来自法国里昂大学、北航和大连理工的论文“Robotic Manipulation via Imitation Learning: Taxonomy, Evolution, Benchmark, and Challenges”。 机器人操作 (RM) 是使自主机器人能够在真实场景中与环境互动并改变环境的核心。在众多学习范式中,模仿学习已成为一种强大的方法,它使机器人能够通过人类的演示快速学习复杂的操作技能。本综述首次系统地回顾机器人操作的模仿学习。识别并分析大量具有代表性的研究,这些研究均因其科学质量和学术影响力而被选中。对于每项研究,都提供结构化的总结,涵盖研究目的、技术实现、分类、输入格式、先验知识、优势、局限性和引用指标。除了进行分类之外,还追溯机器人操作策略(RMP)中模仿学习技术的演进历程,重点介绍关键的方法论转变——从扩散和流匹配到自回归和affordance驱动策略。尽可能地收集基准测试结果并进行定量比较,从而能够对跨任务和环境的性能进行综合分析。最后,概述诸如泛化、具身多样性、数据效率和基准标准化等尚未解决的挑战,并探讨构建可扩展和通用型RMP的潜方向。 机

By Ne0inhk
无人机遥感航拍巡检数据集 无人机遥感图像识别 无人机视角山区泥石流和滑坡图像识别数据集-数据集第10067期

无人机遥感航拍巡检数据集 无人机遥感图像识别 无人机视角山区泥石流和滑坡图像识别数据集-数据集第10067期

滑坡检测数据集核心信息介绍 ** 这个滑坡检测数据集主要用于目标检测任务,整体数据规模和细节都比较明确。从数量上看,数据集总共包含 1660 张图像, 往期热门主题 主题搜两字"关键词"直达 代码数据获取: 获取方式:***文章底部卡片扫码获取*** 覆盖了YOLO相关项目、OpenCV项目、CNN项目等所有类别, 覆盖各类项目场景(包括但不限于以下----欢迎咨询定制): 项目名称项目名称基于YOLO+deepseek 智慧农业作物长势监测系统基于YOLO+deepseek 人脸识别与管理系统基于YOLO+deepseek 无人机巡检电力线路系统基于YOLO+deepseek PCB板缺陷检测基于YOLO+deepseek 智慧铁路轨道异物检测系统基于YOLO+deepseek 102种犬类检测系统基于YOLO+deepseek 人脸面部活体检测基于YOLO+deepseek 无人机农田病虫害巡检系统基于YOLO+deepseek 水稻害虫检测识别基于YOLO+deepseek 安全帽检测系统基于YOLO+deepseek 智慧铁路接触网状态检测系统基于YOLO+

By Ne0inhk
RISC-V开源处理器实战:从Verilog RTL设计到FPGA原型验证

RISC-V开源处理器实战:从Verilog RTL设计到FPGA原型验证

引言:开源浪潮下的RISC-V处理器设计 在芯片设计领域,RISC-V架构正以其开源免授权、模块化扩展和极简指令集三大优势重塑行业格局。与传统闭源架构不同,RISC-V允许开发者自由定制处理器核,从嵌入式微控制器到高性能服务器芯片均可覆盖。本文以Xilinx Vivado 2025工具链和蜂鸟E203处理器为核心,完整呈现从Verilog RTL设计到FPGA原型验证的全流程,为嵌入式工程师和硬件爱好者提供一套可复现的实战指南。 项目目标与技术栈 * 核心目标:基于RISC-V RV32I指令集,设计支持五级流水线的32位处理器核,实现基础算术运算、逻辑操作及访存功能,并在Xilinx Artix-7 FPGA开发板验证。 * 工具链:Xilinx Vivado 2025(逻辑设计、综合实现)、ModelSim(功能仿真)、Xilinx Artix-7 XC7A35T FPGA开发板(硬件验证)。 * 参考案例:蜂鸟E203处理器(芯来科技开源RISC-V核,已在Xilinx FPGA上完成移植验证,最高运行频率50MHz)。 一、数字系统设计流程:从需求到架构 1.

By Ne0inhk

快速掌握PyMAVLink:无人机通信的Python实战指南

快速掌握PyMAVLink:无人机通信的Python实战指南 【免费下载链接】pymavlinkpython MAVLink interface and utilities 项目地址: https://gitcode.com/gh_mirrors/py/pymavlink 在当今无人机技术飞速发展的时代,PyMAVLink作为Python实现的MAVLink协议库,已经成为连接地面站与飞行器的关键桥梁。这个强大的工具库不仅简化了无人机通信的复杂性,更为开发者提供了完整的解决方案。 从零开始:PyMAVLink架构解析 核心通信层设计 PyMAVLink的核心架构围绕MAVLink协议栈构建,通过mavutil.py模块实现了与无人机的高效通信。该模块提供了多种连接方式,包括串口、TCP/UDP网络连接等,确保开发者能够灵活应对不同的应用场景。 协议版本支持:项目支持完整的MAVLink协议版本演进,从v0.9到v2.0,每个版本都有对应的dialects目录实现。这种分层架构确保了向后兼容性和协议扩展性。 多语言代码生成引擎 generator/mavgen.py是整

By Ne0inhk