【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

告别重复劳动:用AI数据标注工具提速3倍的实战经验

告别重复劳动:用AI数据标注工具提速3倍的实战经验

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕AI这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * 告别重复劳动:用AI数据标注工具提速3倍的实战经验 * 为什么数据标注是“效率黑洞”? * AI标注工具的核心优势:不只是快,更是智能 * 实战经验:从0到1的AI标注落地 * 项目背景:一个真实的数据标注挑战 * 工具集成:代码示例详解 * 步骤1:安装依赖库 * 步骤2:加载预训练模型(使用PyTorch) * 步骤3:集成到Label Studio工作流 * 步骤4:人工审核界面优化 * 速度与质量实测数据 * 流程优化:用Mermaid重构标注工作流 * 避坑指南:实战中的常见陷阱 * 陷阱1:AI模型不匹配业务场景 * 陷阱2:数据格式不兼容

By Ne0inhk
手把手教你:在 Windows 部署 OpenAkita 并接入飞书模块,实现真正能干活的本地 AI 助手

手把手教你:在 Windows 部署 OpenAkita 并接入飞书模块,实现真正能干活的本地 AI 助手

目 录 * 前言 * 第一章:为什么选 OpenAkita,而不是直接用 OpenClaw? * 1.1 当前 AI 助理的几个现实痛点 * 1.2 OpenAkita 的核心优势(对比 OpenClaw) * 1.3 谁最适合用 OpenAkita? * 第二章:Windows 下安装 OpenAkita(两种方案) * 2.1 准备工作 * 2.2 方案一:一键脚本安装(适合能接受 PowerShell 的用户) * 2.3 方案二:桌面安装包(最像普通软件,新手友好) * 第三章:配置蓝耘(Lanyun)平台 API 密钥

By Ne0inhk
内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:未来思考,本专栏结合当前国家战略和实时政治,对未来行业发展的思考 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 🔥内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解 |前言| 最近装机的小伙伴们欲哭无泪:DDR5内存价格一路狂飙,部分DRAM现货价格在过去一年暴涨近700% 。大家习惯性吐槽“厂商放火”、“产能不足”,但很少有人看到,这场涨价风暴的真正推手,是那只名为“AI”的巨兽。 当你还在为多花几百块钱买内存心疼时,国家正在西部荒漠建起一座座数据中心,科技巨头正在为“吃电怪兽”抢购每一颗芯片。2026年,大型科技公司的AI相关投资预计将达到6500亿美元,较去年增长约80% 。 今天,我们从能源供应、隐私安全、绿色AI 三个维度,结合东数西算、算电协同、

By Ne0inhk