当前位置: 首页 > news >正文

室友入职离职全手册:线程创建・终止・等待底层逻辑 + C/C++ 双语言实战》

搞懂了进程地址空间与线程的内存模型,我们顺着「合租公寓」的比喻,把线程控制的核心三件事一次性讲明白:怎么招新室友(线程创建)、室友怎么退租(线程终止)、退租后怎么收尾(线程等待)。搭配流程图、C/C++ 双语言可运行代码和思维导图,看完就能彻底掌握线程的完整生命周期。


一、线程创建:给公寓招一位新室友

线程创建,本质就是在现有进程(公寓)里新增一个独立执行流,相当于招一位新室友入住。操作系统会给新线程分配独有的私人资源,同时让它共享公寓的所有公共区域。

1. 创建时操作系统做了什么?

你只需要调用一个创建函数,背后操作系统会完成四件事:

  1. 在进程地址空间的栈区,给新线程分配一块独立的线程栈(私人卧室)
  2. 在内核里创建线程控制块(TCB),记录线程 ID、优先级、寄存器状态等信息
  3. 把你指定的「入口函数」和「参数」放到新线程的栈里,相当于给室友布置任务
  4. 把线程加入 CPU 调度队列,等待分配时间片开始执行

这也解释了为什么线程创建比进程快得多:进程要复制一整套地址空间(相当于盖新公寓),而线程只需要加个卧室和工牌,公共区域全复用。

2. C 语言 POSIX 实现

Linux 下标准线程库pthread的创建函数是pthread_create,四个参数刚好对应招室友的完整流程:

c

运行

int pthread_create( pthread_t *tid, // 输出参数:新线程的ID(给室友发工牌) const pthread_attr_t *attr, // 线程属性:栈大小、优先级等,默认填NULL void *(*start_routine)(void *), // 线程入口函数(室友的本职工作) void *arg // 传给入口函数的参数(给室友的工具/任务) );

可运行代码示例:创建两个子线程分别执行任务

c

运行

#include <stdio.h> #include <pthread.h> #include <unistd.h> // 线程1的任务:打扫卫生 void *clean_task(void *arg) { char *name = (char *)arg; printf("[%s] 开始打扫公寓...\n", name); sleep(2); printf("[%s] 打扫完成!\n", name); return NULL; } // 线程2的任务:做饭 void *cook_task(void *arg) { char *name = (char *)arg; printf("[%s] 开始做饭...\n", name); sleep(3); printf("[%s] 饭做好了!\n", name); return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, clean_task, "室友A"); pthread_create(&t2, NULL, cook_task, "室友B"); printf("主线程:两个室友都开始干活了\n"); sleep(5); printf("主线程:公寓一天结束\n"); return 0; }

编译命令:gcc demo.c -o demo -lpthread

3. C++ 标准库实现

C++11 及以上提供了std::thread,底层仍封装 pthread,语法更安全易用:

cpp

运行

#include <iostream> #include <thread> #include <unistd.h> void clean_task(const std::string& name) { std::cout << "[" << name << "] 开始打扫公寓..." << std::endl; sleep(2); std::cout << "[" << name << "] 打扫完成!" << std::endl; } void cook_task(const std::string& name) { std::cout << "[" << name << "] 开始做饭..." << std::endl; sleep(3); std::cout << "[" << name << "] 饭做好了!" << std::endl; } int main() { std::thread t1(clean_task, "室友A"); std::thread t2(cook_task, "室友B"); std::cout << "主线程:两个室友都开始干活了..." << std::endl; t1.join(); t2.join(); std::cout << "主线程:公寓一天结束" << std::endl; return 0; }

编译命令:g++ demo.cpp -o demo -std=c++11 -pthread注意:std::thread对象析构前必须调用join()detach(),否则程序直接崩溃。

4. 经典踩坑:给线程传局部变量

很多初学者会写出这样的 bug:在循环里创建线程,把循环变量的地址传进去。

c

运行

// ❌ 错误写法 for(int i=0; i<3; i++){ pthread_create(&tid, NULL, task, &i); }

用公寓比喻很好理解:你在自己床头柜上写了个数字,转头就改,然后让室友去看这个数字。等室友真正去看的时候,数字早就变了,甚至你已经离开房间把变量销毁了。

✅ 正确做法:传值(强制转成 void*)或者动态分配内存传进去;C++ 中优先按值传参,引用必须用std::ref包装且保证变量生命周期长于线程。


二、线程终止:室友的三种「退租方式」

线程终止就是室友结束任务、离开公寓。终止方式有三种,体面程度不同,但都只会让线程自己离开,不会影响公寓和其他室友(除非搞破坏)。

方式 1:入口函数 return —— 体面主动退场

线程函数执行完return返回,相当于室友干完活,收拾好东西主动关门走人。这是最推荐、最安全的终止方式,返回值可以被其他线程获取,局部变量自动销毁。

方式 2:调用退出函数 —— 随时主动退房

在线程函数的任意位置调用退出函数,线程立刻终止,也可以携带返回值。

  • C 语言:pthread_exit(返回值)
  • C++:函数内任意位置return即可,不推荐暴力终止,优先用标志位协作退出

return的区别:return只是从当前函数返回,外层还有函数的话会继续执行;pthread_exit直接终止整个线程,不管嵌套了多少层函数。

方式 3:被其他线程取消 —— 被动劝退

一个线程可以让另一个线程终止,相当于告诉室友 “你别干了,可以走了”。

  • C 语言:pthread_cancel(tid),注意取消不是立刻生效,线程需要走到「取消点」才会真正退出。
  • C++:标准库无强制取消接口,通用做法是用原子标志位协作式退出,避免资源泄漏。

❌ 绝对禁忌:在线程里调用 exit ()

exit()是终止整个进程的!不是终止线程。就像室友闹脾气,直接把整栋公寓炸了,所有室友全部强制退场。这是多线程编程里的经典低级错误。

C++ 协作式取消示例

cpp

运行

#include <iostream> #include <thread> #include <atomic> #include <unistd.h> std::atomic<bool> is_running = true; void work_task() { while(is_running) { std::cout << "线程正在工作..." << std::endl; sleep(1); } std::cout << "线程收到退出信号,正常退场" << std::endl; } int main() { std::thread t(work_task); sleep(3); is_running = false; t.join(); std::cout << "主线程:线程资源已回收" << std::endl; return 0; }

三、线程等待:等室友交接完再收房

线程等待的作用是:阻塞等待指定线程结束,获取它的返回值,并回收线程的所有系统资源

用公寓比喻就是:室友说要走了,你站在卧室门口等他出来,接过他交还的钥匙和工作成果(返回值),然后进去打扫房间、回收床位(释放栈和 TCB 资源)。如果不等待回收,线程会变成「僵尸线程」,越积越多最终占满资源。

1. C 语言 POSIX 实现

函数pthread_join,阻塞等待目标线程终止并回收资源:

c

运行

int pthread_join( pthread_t tid, // 要等待的线程ID void **retval // 二级指针,接收线程的返回值 );

基础等待示例

c

运行

int main() { pthread_t t1, t2; pthread_create(&t1, NULL, clean_task, "室友A"); pthread_create(&t2, NULL, cook_task, "室友B"); printf("主线程:等待室友们干完活...\n"); pthread_join(t1, NULL); printf("主线程:室友A已退场\n"); pthread_join(t2, NULL); printf("主线程:室友B已退场\n"); printf("主线程:所有人都走了,公寓关门\n"); return 0; }

获取返回值示例

c

运行

// 子线程:返回计算结果 void *calc_task(void *arg) { int num = *(int*)arg; int *result = malloc(sizeof(int)); *result = num * 2; pthread_exit(result); } // 主线程:接收结果 int main() { pthread_t tid; int n = 10; pthread_create(&tid, NULL, calc_task, &n); void *ret; pthread_join(tid, &ret); int *res = (int*)ret; printf("计算结果:%d\n", *res); free(res); return 0; }

2. C++ 标准库实现

C++ 不支持join直接拿返回值,推荐两种主流方案:

方案 A:引用传参输出结果(简单直观)

cpp

运行

#include <iostream> #include <thread> void calc_task(int num, int& result) { result = num * 2; } int main() { int result = 0; std::thread t(calc_task, 10, std::ref(result)); t.join(); std::cout << "计算结果:" << result << std::endl; return 0; }
方案 B:std::promise + std::future(标准异步方案)

对应 C 版本pthread_exit携带返回值的能力,是 C++ 官方推荐的线程间安全传值方式。

cpp

运行

#include <iostream> #include <thread> #include <future> void calc_task(int num, std::promise<int> prom) { int res = num * 2; prom.set_value(res); } int main() { std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread t(calc_task, 10, std::move(prom)); int result = fut.get(); std::cout << "计算结果:" << result << std::endl; t.join(); return 0; }

四、线程分离:不用等的自主退房

如果有些后台线程(比如日志线程),我们不需要它的返回值,也不想专门等它收尾,可以设置为分离线程

比喻:室友入住时就说好 “我走的时候自己打扫干净房间,不用你送”。线程终止后,操作系统自动回收资源,不需要join。分离后的线程不能再被 join,否则报错,适合不需要交互的后台常驻线程。

C 语言实现

c

运行

pthread_detach(tid); // 设置线程分离

C++ 实现

cpp

运行

#include <iostream> #include <thread> #include <unistd.h> void daemon_task() { while(true) { std::cout << "[后台线程] 正在巡检公寓..." << std::endl; sleep(1); } } int main() { std::thread t(daemon_task); t.detach(); std::cout << "主线程:前台业务运行中..." << std::endl; sleep(5); std::cout << "主线程:前台业务结束,程序退出" << std::endl; return 0; }

五、线程生命周期全流程图解

我们把创建、运行、终止、等待回收整个流程串起来,形成完整的线程生命周期:

六、全文知识思维导图

最后用一张思维导图把线程控制的核心知识点全部串起来,方便复习巩固:

最后一句话总结

线程创建是给公寓添人,只加私人卧室、复用公共空间,成本极低;线程终止是人走了但资源没清;线程等待是收尾回收、交接成果。所有线程控制机制,本质都是在「共享地址空间」这个大前提下,平衡并发效率与资源安全。

谢谢
http://www.jsqmd.com/news/1097001/

相关文章:

  • 前端页面开发|校园二手平台全局公共组件、个人中心页面代码详解
  • el-cascader 动态加载与数据回显实战:从需求拆解到交互优化
  • 你的.lic文件安全吗?深入聊聊smart-license的防篡改机制与常见激活成功教程误区
  • 深入用法示例 + 完整 Visual Studio 项目结构 最常用、最重要的三个容器为例进行深入讲解
  • Windows系统文件AdmTmpl.dll丢失找不到问题解决
  • 别再为系统扰动头疼了!手把手教你用扩张状态观测器网络(ESOnet)搞定复杂不确定性
  • SurroundOcc 实战:从数据加载到可视化,构建端到端3D场景重建流程
  • 山东诺亚创生带您了解脐带胎盘干细胞:被误解的生命初始“建材”
  • 我家的佳能TS5380,打印着作用的时候突然报错5b00,5b02这个故障码,带到维修店维修,说要150元费用,太贵没有就修带回来了,网上说清零软件就可以修好,之后找到 V6.200这个版本的清零软件
  • Gemini 集成 Android Auto 引隐私担忧,这些设置更改让驾车更具隐私性
  • SAP MRP元素全解:从代码到场景的应用指南
  • 终极跨平台文本编辑解决方案:Notepad--让中文编码和文件对比变得简单
  • 【生产环境禁用警告】:VMware磁盘映射到主机的3大高危操作(附vSphere PowerCLI一键检测脚本)
  • 【转帖】高考生注意了!21个投档录取问题汇总
  • 22年网络建设与运维国赛iscsi服务
  • Element Plus虚拟化表格el-table-v2自定义渲染实战:从JS函数到JSX语法的性能与开发体验对比
  • 别再为Shapely安装报错发愁了!手把手教你根据Python版本和系统选对whl文件
  • Windows系统文件AIComponentMgmt.dll丢失找不到问题解决
  • 一人公司必备AI工具:5分钟将详情页变出30条高转化获客笔记
  • Vue二维码组件深度解析:qrcode.vue 3种高效生成方案对比
  • Diablo Edit2:暗黑破坏神II存档编辑器的二进制数据处理革命
  • 高效Minecraft服务器包生成工具:ServerPackCreator深度解析与实战指南
  • GitOps——让Git成为唯一的“真相来源“
  • SAP委外PO:从供应商委外到工序委外的核心逻辑与实战配置
  • 基于第三方API的代购系统对接实现——从商品同步到物流追踪的全链路方案
  • 【HSPICE】从SPICE内核到仿真实战:电路设计的核心引擎
  • 保姆级教程:用MATLAB脚本在STK里一键生成Walker星座(附完整代码)
  • 别再手动点Download了!用Python调用NCBI Datasets API批量下载基因FASTA序列(附完整代码)
  • Three.js 道路流光教程
  • 3步拯救损坏视频:Untrunc视频修复工具终极指南