字节技术总监笔记:linux多线程>>进程线程互斥管道
今天更新:
linux 进程线程>>>>
1 第一大部分:.总笔记融合改进版本>结合豆包表达:
进程-进程创建-退出线程-gdb调试- 进程回收+线程创建-收回-取消这一系列知识点!!!!
模块一:进程创建与回收(fork/wait/waitpid)
核心价值:实现嵌入式系统的 “多任务协作”(如传感器采集、网络通信并行执行)。
一、进程创建:fork()——“克隆自己干多活”
原理通俗解释
fork()是 “克隆进程” 的操作:调用后操作系统会复制当前进程的代码、数据、栈、打开的文件等资源,生成一个 “子进程”。父子进程几乎一样,但有两个关键区别:
父进程的fork()返回子进程的 PID(进程 ID)。
子进程的fork()返回0。
代码实战:最基础的父子进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid; // 存储进程ID的变量// 克隆进程(父进程执行后,子进程也会从这里开始执行)pid = fork();if (pid < 0) {// fork失败(如内存不足)printf("进程创建失败\n");return -1;} else if (pid == 0) {// 子进程分支:pid=0printf("我是子进程,我的PID是 %d,父进程PID是 %d\n", getpid(), getppid());} else {// 父进程分支:pid=子进程的PIDprintf("我是父进程,我的PID是 %d,子进程PID是 %d\n", getpid(), pid);sleep(2); // 父进程休眠2秒,确保子进程先输出}return 0;
}
逐行通俗解释
pid_t pid;:pid_t是进程 ID 的类型(本质是整数),用来区分父子进程。
pid = fork();:执行后,操作系统会 “复制” 当前进程,父子进程从此处并行执行。
if (pid == 0):子进程的逻辑分支,getpid()获取自己的 PID,getppid()获取父进程的 PID。
else:父进程的逻辑分支,pid变量存储的是子进程的 PID。
sleep(2);:父进程休眠 2 秒,确保子进程先打印,方便观察运行顺序。
嵌入式考点
子进程会复制父进程的文件描述符(比如父进程打开的传感器设备,子进程可直接读写)。
父子进程共享文件偏移量(父进程读了传感器前 10 字节,子进程再读会从第 11 字节开始)。
二、进程回收:wait()与waitpid()——“等子进程干完活”
如果父进程不回收子进程,子进程会变成僵尸进程(占用 PID 资源,导致系统无法创建新进程)。wait()和waitpid()是父进程 “等子进程退出并回收资源” 的工具。
代码实战:用wait()回收子进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t pid, wait_pid;int status; // 存储子进程退出状态pid = fork();if (pid < 0) {printf("fork失败\n");return -1;} else if (pid == 0) {// 子进程:模拟“干活3秒”后退出printf("子进程开始干活,3秒后退出\n");sleep(3);printf("子进程干完活,退出\n");return 123; // 子进程退出码(自定义,0表示正常)} else {// 父进程:等子进程退出printf("父进程开始等子进程退出...\n");wait_pid = wait(&status); // 阻塞等待,直到子进程退出if (WIFEXITED(status)) { // 判断子进程是否正常退出printf("子进程 %d 正常退出,退出码是 %d\n", wait_pid, WEXITSTATUS(status));} else {printf("子进程异常退出\n");}}return 0;
}
逐行通俗解释
#include <sys/wait.h>:wait和waitpid的头文件。
int status;:存储子进程的退出状态(需用宏解析)。
return 123;:子进程的 “退出码”,父进程可通过它判断子进程的退出原因。
wait_pid = wait(&status);:父进程阻塞在这里,直到子进程退出。wait_pid是退出的子进程 PID。
WIFEXITED(status):宏,判断子进程是否 “正常退出”(非被信号杀死)。
WEXITSTATUS(status):宏,提取子进程的退出码(如示例中的 123)。
代码实战:用waitpid()灵活回收(指定子进程、非阻塞)
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t pid1, pid2, wait_pid;int status;// 创建两个子进程pid1 = fork();if (pid1 == 0) {printf("子进程1(PID=%d)开始干活,5秒后退出\n", getpid());sleep(5);return 1;}pid2 = fork();if (pid2 == 0) {printf("子进程2(PID=%d)开始干活,2秒后退出\n", getpid());sleep(2);return 2;}// 父进程:先等子进程2,再等子进程1wait_pid = waitpid(pid2, &status, 0); // 阻塞等pid2if (WIFEXITED(status)) {printf("回收子进程2(PID=%d),退出码=%d\n", wait_pid, WEXITSTATUS(status));}wait_pid = waitpid(pid1, &status, 0); // 阻塞等pid1if (WIFEXITED(status)) {printf("回收子进程1(PID=%d),退出码=%d\n", wait_pid, WEXITSTATUS(status));}return 0;
}
嵌入式考点
僵尸进程的危害:嵌入式系统资源有限,大量僵尸进程会导致 PID 耗尽,新进程无法创建。
waitpid的WNOHANG参数:表示 “非阻塞等待”(父进程可边等边处理硬件中断),嵌入式中常用于 “多任务并发监控”。
面试高频问题
问:fork后父子进程的执行顺序?
答:由操作系统调度决定,若要控制顺序,需用wait或信号量同步。
问:嵌入式中用fork要注意什么?
答:嵌入式内存小,fork会复制父进程所有资源,易导致内存不足。建议仅在必要时创建子进程,且子进程尽量少分配内存。
模块二:exec 函数族 ——“换个程序接着跑”
核心价值:在当前进程中运行另一个程序(如嵌入式中 “进程 A 启动固件升级程序 B”)。
原理通俗解释
exec函数族的作用是 **“程序替换”:当前进程的代码、数据、栈等会被新程序完全替换,仅保留进程 ID(PID)和打开的文件描述符 **。
常用exec函数:
execl:“l” 表示参数列表(可变参数传参)。
execv:“v” 表示参数数组(用数组传参)。
execvp:“p” 表示从PATH 环境变量找程序。
代码实战:execl执行系统命令(如ls)
c
运行
#include <stdio.h>
#include <unistd.h>int main() {printf("执行前:当前进程PID=%d\n", getpid());// 执行ls -l命令(execl参数格式:程序路径, 程序名, 参数1, 参数2, ..., NULL)execl("/bin/ls", "ls", "-l", NULL);// 若execl成功,下面代码不会执行(程序已被替换)printf("若看到这句话,说明execl执行失败\n");return 0;
}
逐行通俗解释
execl("/bin/ls", "ls", "-l", NULL);:
第一个参数/bin/ls是程序的绝对路径。
第二个参数"ls"是程序名(传入新程序的argv[0])。
第三个参数"-l"是命令行参数。
必须以NULL结尾,标记参数列表结束。
printf("若看到这句话..."):若execl成功,当前进程代码被ls替换,这句话不会执行;若失败(如路径错误)才会执行。
代码实战:execv执行自定义程序
假设我们有一个hello.c:
c
运行
// hello.c
#include <stdio.h>int main(int argc, char* argv[]) {printf("我是新程序,PID=%d\n", getpid());if (argc > 1) {printf("收到参数:%s\n", argv[1]);}return 0;
}
编译为hello后,用execv执行:
c
运行
#include <stdio.h>
#include <unistd.h>int main() {char* argv[] = {"hello", "嵌入式14koffer", NULL}; // 参数数组,以NULL结尾printf("执行前:PID=%d\n", getpid());execv("./hello", argv); // 执行当前目录下的hello程序printf("execv执行失败\n");return 0;
}
嵌入式考点
exec会保留打开的文件描述符:父进程打开的传感器设备,替换程序后仍可访问。
嵌入式中常用于启动 “工具程序”(如固件升级脚本、硬件诊断程序)。
面试高频问题
问:exec成功后,原进程的代码还在吗?
答:不在了,原进程的代码、数据、栈会被新程序完全替换,仅保留 PID 和打开的文件描述符。
模块三:守护进程 ——“后台默默干活的服务”
核心价值:实现嵌入式系统的 “后台服务”(如传感器采集服务、网络心跳守护)。
原理通俗解释
守护进程是在后台运行、脱离终端、不受用户登录退出影响的进程。创建步骤:
fork子进程,父进程退出(子进程成为孤儿进程,被init进程收养)。
子进程调用setsid,创建新会话,脱离原终端。
改变工作目录(防止原目录被卸载)。
重定向标准输入 / 输出 / 错误到/dev/null(避免终端干扰)。
代码实战:创建简单守护进程
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void create_daemon() {pid_t pid;// 步骤1:fork子进程,父进程退出pid = fork();if (pid > 0) {printf("父进程退出,PID=%d\n", getpid());return;} else if (pid < 0) {perror("fork失败");return;}// 子进程继续执行(此时是孤儿进程,被init收养)printf("子进程启动,PID=%d,父进程PID=%d(init进程)\n", getpid(), getppid());// 步骤2:创建新会话,脱离原终端setsid();// 步骤3:改变工作目录到根目录(/)chdir("/");// 步骤4:重定向标准输入/输出/错误到/dev/nullint fd = open("/dev/null", O_RDWR);dup2(fd, STDIN_FILENO); // 标准输入重定向到/dev/nulldup2(fd, STDOUT_FILENO); // 标准输出重定向到/dev/nulldup2(fd, STDERR_FILENO); // 标准错误重定向到/dev/nullclose(fd);// 守护进程核心逻辑:模拟“每10秒写一次日志”while (1) {sleep(10);printf("守护进程在后台干活...\n"); // 输出被重定向到/dev/null,实际看不到}
}int main() {create_daemon();return 0;
}
逐行通俗解释
pid = fork(); if (pid > 0) return;:父进程退出,子进程成为孤儿进程,由init进程(PID=1)收养。
setsid();:子进程创建新会话,成为会话组长,彻底脱离原终端(否则终端关闭会导致进程退出)。
chdir("/");:防止原工作目录被卸载(如 U 盘挂载的目录)。
open("/dev/null", O_RDWR); dup2(...):/dev/null是 “黑洞设备”,所有输入输出会被丢弃,确保守护进程不被终端干扰。
嵌入式考点
守护进程的日志输出:不能用printf(会被重定向),需写入专门的日志文件(如/var/log/sensor.log)。
嵌入式常用场景:网络心跳检测、硬件看门狗、自动升级服务。
面试高频问题
问:守护进程为什么要fork两次?
答:第一次fork让子进程成为孤儿进程(被init收养);第二次fork(部分实现)让进程不再是会话组长,彻底避免终端影响。
模块四:GDB 调试多进程程序 ——“同时盯多个任务”
核心价值:嵌入式开发中需同时调试父子进程或多个独立进程,GDB 多进程调试是必备技能。
调试步骤:以父子进程为例
先写一个多进程程序multi_process.c:
c
运行
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid;pid = fork();if (pid == 0) {// 子进程:循环打印for (int i = 0; i < 5; i++) {printf("子进程:i=%d\n", i);sleep(1);}} else {// 父进程:循环打印for (int i = 0; i < 5; i++) {printf("父进程:i=%d\n", i);sleep(1);}wait(NULL);}return 0;
}
编译时加调试信息:gcc -g -o multi_process multi_process.c
步骤 1:启动 GDB 并设置跟踪模式
bash
gdb ./multi_process
(gdb) set follow-fork-mode child # 跟踪子进程(默认跟踪父进程)
(gdb) run
步骤 2:查看当前调试的进程
gdb
(gdb) info inferiorsNum PID Status Command
* 1 1235 running ./multi_process
*表示当前正在调试的进程(子进程)。
步骤 3:切换调试进程
gdb
(gdb) inferior 2 # 假设父进程的inferior编号是2(需先找到父进程PID)
步骤 4:设置断点、单步调试
在子进程的printf处设断点:
gdb
(gdb) b multi_process.c:8
(gdb) c # 继续运行,直到断点
嵌入式调试技巧
若要调试多个独立进程,可先启动 GDB,再用attach 进程PID附加到目标进程。
嵌入式板卡上调试多进程时,结合gdbserver的--multi模式,支持同时调试多个进程。
模块五:线程创建与参数传递(pthread_create)
核心价值:实现嵌入式系统的 “轻量级多任务”(如实时数据处理、硬件中断响应)。
原理通俗解释
线程是进程内的 “子任务”,共享进程的内存空间(代码、数据、堆、打开的文件),但有独立的栈。pthread_create用于创建线程,参数包括:线程 ID、线程属性、线程函数、线程函数的参数。
代码实战:创建线程并传递参数
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 线程函数:接收void*参数,返回void*
void* thread_func(void* arg) {int* num = (int*)arg; // 强转为int指针,获取参数printf("线程启动,收到参数:%d\n", *num);// 线程核心逻辑:模拟处理传感器数据for (int i = 0; i < 3; i++) {printf("线程干活中:%d\n", i);sleep(1);}return (void*)0; // 线程退出码(0表示正常)
}int main() {pthread_t tid; // 线程IDint arg = 123; // 要传递给线程的参数// 创建线程:参数为线程ID、默认属性、线程函数、参数int ret = pthread_create(&tid, NULL, thread_func, &arg);if (ret != 0) {printf("线程创建失败\n");return -1;}printf("主线程继续干活,等待线程退出\n");sleep(5); // 主线程休眠,确保线程执行完return 0;
}
逐行通俗解释
void* thread_func(void* arg):线程函数格式固定,参数和返回值都是void*(可传递任意类型数据)。
int* num = (int*)arg;:将void*强转为int*,获取传递的参数。
pthread_t tid;:线程 ID 的类型(与进程 PID 不同,是线程的唯一标识)。
pthread_create(&tid, NULL, thread_func, &arg);:
第一个参数&tid:存储线程 ID。
第二个参数NULL:使用默认线程属性。
第三个参数thread_func:线程要执行的函数。
第四个参数&arg:传递给线程函数的参数地址。
sleep(5);:主线程休眠 5 秒,确保子线程有时间执行(否则主线程提前退出,子线程也会被终止)。
代码实战:传递复杂参数(结构体)
嵌入式中常需传递 “多个关联参数”(如传感器 ID、采样率),用结构体封装更方便。
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 定义参数结构体
typedef struct {int sensor_id; // 传感器IDint sample_rate; // 采样率(Hz)char buffer[100];// 数据缓冲区
} SensorParam;void* thread_func(void* arg) {SensorParam* param = (SensorParam*)arg; // 强转为结构体指针printf("线程启动:传感器ID=%d,采样率=%d\n", param->sensor_id, param->sample_rate);// 模拟采集数据sprintf(param->buffer, "传感器%d的采集数据", param->sensor_id);printf("采集数据:%s\n", param->buffer);return (void*)0;
}int main() {pthread_t tid;SensorParam param = {1, 10, {0}}; // 初始化参数pthread_create(&tid, NULL, thread_func, ¶m);printf("主线程等待...\n");sleep(2);return 0;
}
嵌入式考点
线程共享进程的文件描述符:主线程打开的传感器设备,子线程可直接读写。
线程共享全局变量:需加锁(如pthread_mutex_t)防止竞争。
面试高频问题
问:线程和进程的区别?
答:进程是资源分配单位,线程是调度执行单位;进程间内存独立,线程共享进程内存;进程创建开销大,线程创建开销小。
问:嵌入式中什么时候用线程,什么时候用进程?
答:
线程:需要高并发、低延迟(如实时数据处理),且任务间需共享内存 / 资源。
进程:任务间需要隔离(一个任务崩溃不影响其他任务),或需要运行独立程序(如通过exec启动)。
模块六:线程回收与内存管理(pthread_join)
核心价值:避免线程内存泄漏,确保嵌入式系统长期稳定运行。
原理通俗解释
pthread_join是线程版的 “wait”:阻塞等待指定线程退出,回收其栈资源,并获取线程的退出状态。
代码实战:用pthread_join回收线程
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* thread_func(void* arg) {printf("线程启动,开始干活\n");sleep(3); // 模拟干活3秒printf("线程干完活,退出\n");return (void*)123; // 线程退出码
}int main() {pthread_t tid;void* ret_val; // 存储线程退出状态pthread_create(&tid, NULL, thread_func, NULL);// 回收线程:阻塞等待,直到线程退出pthread_join(tid, &ret_val);printf("线程退出,退出码=%d\n", (int)ret_val);return 0;
}
代码实战:线程内存管理(避免泄漏)
c
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>void* thread_func(void* arg) {// 线程内分配堆内存int* data = (int*)malloc(sizeof(int));*data = 123;printf("线程分配内存:%p,值=%d\n", data, *data);// 线程退出前必须释放,否则内存泄漏free(data);printf("线程释放内存\n");return (void*)0;
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, NULL);printf("主线程结束,内存无泄漏\n");return 0;
}
嵌入式考点
线程退出后,栈资源会自动回收,但堆内存(如malloc分配的)需手动free,否则会内存泄漏。
嵌入式中可用valgrind --leak-check=full ./prog检测内存泄漏。
面试高频问题
问:pthread_join和wait的区别?
答:pthread_join回收线程资源,wait回收进程资源;线程是进程内的执行单元,进程是独立的资源单位。
问:嵌入式中如何避免线程内存泄漏?
答:线程内分配的堆内存退出前必须free;用内存池管理频繁分配的内存;集成valgrind或自定义工具定期扫描泄漏。
总结
掌握进程创建与回收、exec 函数族、守护进程、GDB 多进程调试、线程创建与回收后,你已具备嵌入式 “多任务编程” 的核心能力。结合嵌入式场景的资源限制、硬件交互、实时性要求,完全可以胜任珠三角 14k + 的嵌入式开发岗位。2 第二大部分:原始学习:手写版本知识点+涉及代码
1 线程进程知识点总结:
共享的东西:
公有:
全局变量、进程id、用户id 、group ID、
私有的:
threadID \ pc计数器、堆栈、errno、优先级、status属性

(2)部分线程/进程功能,要通过pthread通过外挂库!!!
(3)pthread的create函数:创建一个线程>>>>运行的结果不确定!

跑这个代码,每次执行结果不一样?????

要么是 主函数 要么是两个都有,要么只有thread函数!!!!????
因为加了一个sleep直接就让主函数停止等待一秒钟!!!!

pthread_exit(NULL)ky 让线程直接先退出去!!!!!
(4)线程id
测试一个nano-sec的函数,高精度时间戳:


代码:
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>void* testFun(void* arg) {// 线程函数内容return NULL;
}int main() {pthread_t tid;void* args = NULL;int ret = pthread_create(&tid, NULL, testFun, &args);if (ret != 0) {printf("thread 创建失败!!\n");return 0;}// pthread_join(tid, NULL);printf("this is main thread !!!!!!!\n");printf(">>>>>>>start time count !!!!\n");struct timespec start, end;clock_gettime(CLOCK_MONOTONIC, &start);for (int i = 0; i < 10000; i++) { }clock_gettime(CLOCK_MONOTONIC, &end);long sec_time = -start.tv_sec + end.tv_sec;long sev_nanotime = end.tv_nsec - start.tv_nsec;printf("********* time is :%lu \n. nano sec is %lu \n *********\n", sec_time, sev_nanotime);printf("<<<<<<<<<<<<<<<<<<<<<<<<endl tiem count now!!!!!\n\n");sleep(1);return 0;
}结果如图所示!

纳秒级程序前后差异!!!
(5)参数传递:
重难点!!!

插一句:动态性!!!!!!
传参数怎么搞??
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<time.h>void * testFun(void* args){printf("in the function of testFun \n");printf("pid is %d \n,pthread id is : %lu \n",getpid(),pthread_self());printf(" the data input is : %d \n",*(int*)args);pthread_exit(NULL);printf("after pthread exit \n");
}int main(){pthread_t tid ; int args = 5;int ret= pthread_create(&tid,NULL,testFun,&args);if(ret!=0){printf("thread 创建失败!!\n");return 0 ;}// pthread_join(tid,NULL);printf("1 this is main thread !!!!!!!\n");sleep(1);printf(" >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>start time count !!!!\n");struct timespec start ,end ;clock_gettime(CLOCK_MONOTONIC,&start);for(int i= 0 ;i<10000;i++){ }clock_gettime(CLOCK_MONOTONIC,&end);long sec_time = -start.tv_sec+end.tv_sec;long sev_nanotime = end.tv_nsec-start.tv_nsec;//printf("*********** time is :%lu \n, nano sec is %lu \n **********\n",sec_time,sev_nanotime);printf("<<<<<<<<<<<<<<<<<<<<<<<end tiem count now!!!!!\n\n");sleep(1);}
为什么???
多线程这样了????
1. 线程中输出的i值混乱(不符合循环变量的预期)
代码问题:
在main函数中,循环创建线程时,传递的参数是循环变量i的地址:
c
运行
for(int i = 0; i < 5; i++){int ret = pthread_create(&tid, NULL, testFun, (void *)&i);
// 传递&i
}此时存在竞态条件:线程创建后,main函数的循环可能继续执行(i的值会被修改)
,而新线程可能还未读取i的值。因此,线程中实际读取到的i值可能已经被循环
更新,导致输出的i与预期的 “0,1,2,3,4” 不符(例如你的输出中出现了 2、2、3、4、5)。解决方法:为每个线程传递独立的参数副本,而非共享的循环变量地址。例如:
c
运行
for(int i = 0; i < 5; i++){int *arg = malloc(sizeof(int)); // 为每个线程分配独立内存*arg = i;pthread_create(&tid, NULL, testFun, (void *)arg);
}
// 线程函数中使用后释放内存:free(args);竞态条件!!!
多线程会这样!!
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
好了, 讲完了线程进程创建,接下来开始讲回收!!!
(6)线程回收:pthread _ join( )

例程:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // 用于malloc/free(嵌入式中需注意堆大小)// 子线程任务:计算数组求和(模拟嵌入式中传感器数据汇总)
void *sum_array(void *arg) {int *array = (int*)arg; // 传入的数组(假设前n个元素有效,第n个为结束标记-1)int *result = (int*)malloc(sizeof(int)); // 用堆内存存结果(关键!避免栈释放)*result = 0;for (int i = 0; array[i] != -1; i++) {*result += array[i];}printf("子线程(tid=%lu)计算完毕,退出\n", pthread_self());pthread_exit((void*)result); // 传递堆内存地址作为返回值
}int main() {// 模拟传感器数据:[10, 20, 30, -1](-1为结束标记)int sensor_data[] = {10, 20, 30, -1};pthread_t tid;// 1. 创建线程int create_ret = pthread_create(&tid, NULL, sum_array, sensor_data);if (create_ret != 0) {printf("线程创建失败!错误码:%d(嵌入式中需记录日志,避免系统崩溃)\n", create_ret);return -1;}// 2. 回收线程(阻塞等待)void *retval; // 用于接收子线程返回值int join_ret = pthread_join(tid, &retval);if (join_ret != 0) {printf("线程回收失败!错误码:%d(需排查线程是否已分离或被重复回收)\n", join_ret);return -1;}// 3. 处理返回值(嵌入式中需手动释放堆内存,否则泄漏)int *sum = (int*)retval;printf("传感器数据总和:%d\n", *sum);free(sum); // 释放子线程malloc的内存(核心!嵌入式内存泄漏是致命的)return 0;
}
(7)phread_join()特点>>>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdint.h> // 用于intptr_t转换void* fun(void* args) {printf("这是fun函数里面的东东:\n");sleep(3);// 从动态内存中读取i值(正确,每个线程拿到独立的i副本)int x = *(int*)args; free(args); // 用完动态内存后立即释放(避免泄漏)// 返回x的值(通过intptr_t转换为void*,而非地址)pthread_exit((void*)(intptr_t)x);
}int main(int argc, char* args[]) {pthread_t tid[10];void* retValue;for (int i = 0; i < 5; i++) {// 为每个i分配独立的动态内存(正确)int* ptr = (int*)malloc(sizeof(int));*ptr = i; // 存储当前i值pthread_create(&tid[i], NULL, fun, ptr);}for (int i = 0; i < 5; i++) {pthread_join(tid[i], &retValue);// 将返回的“值”转换为int(正确)int recv_i = (intptr_t)retValue;printf("thread ret = %d \n", recv_i);}sleep(1);return 0;
}1. 为什么return (void*)(intptr_t)x是合法的?
首先明确:pthread_exit的参数类型是void*(通用指针),所以必须返回一个指针类型。但我们的目标是传递整数x的值(而非地址),这就需要利用 C 语言的整数与指针的转换规则。
核心原理:intptr_t的作用
C 语言标准中定义了intptr_t类型(需要包含<stdint.h>),它是一种整数类型,且有一个关键特性:intptr_t的长度与当前系统的指针长度完全一致(32 位系统占 4 字节,64 位系统占 8 字节)。
因此:
当我们把int x转换为intptr_t时,不会发生数据截断(因为int的范围通常小于intptr_t);
再将intptr_t转换为void*(指针类型)时,由于长度一致,转换是 “安全的”(不会丢失信息)。
为什么不是(void*)(int)x?
直接写(void*)(int)x在部分系统中可能有风险:
若系统是 64 位(指针占 8 字节),而int是 32 位,此时(void*)(int)x会将 32 位整数强制转换为 8 字节指针,可能导致高位填充错误(虽然多数编译器能兼容,但不符合标准)。
而(void*)(intptr_t)x通过intptr_t中转,保证了整数与指针的长度匹配,是标准推荐的安全转换方式。
本质:用void*的 “壳” 传递整数 “值”
这里的void*并不是真的 “指向某块内存的指针”,而是借用void*的类型来传递整数的值。
线程函数中:x是整数,通过intptr_t转换为void*,本质是把x的值 “打包” 成指针类型传递出去;
主线程中:再通过(intptr_t)retValue把指针 “解包” 回整数,最终得到x的原始值。
2. 对比:返回 “值” vs 返回 “指针”
场景 正确做法 错误做法 本质区别
返回整数 “值” return (void*)(intptr_t)x; return &x;(返回局部变量地址) 前者传递 “值”,后者传递 “无效地址”
返回动态内存 “指针” return args;(args是malloc的指针) return &x;(同上) 前者指针指向有效内存,后者指向已释放内详细解释:

retValue是一个void*的值,可以转换成int!!!
(8 ) 线程的取消:
逻辑:
两个打工人,main包工头,worker是工人:

代码:
逐行解释(带逻辑分析)
1. 头文件
c
运行
#include <pthread.h> // 线程操作的“工具箱”:创建线程、取消线程等函数都在这里
#include <stdio.h> // 输入输出工具箱:printf打印信息用
#include <unistd.h> // 系统操作工具箱:sleep(暂停)函数在这里
2. 子线程函数(worker):工人 B 的工作内容
c
运行
// 线程函数格式固定:返回void*,参数void*(因为线程可能需要返回结果或接收参数)
void* worker(void* arg) { printf("线程启动,开始循环\n"); // 工人B说:“我开始干活了!”while (1) { // 死循环:工人B一直重复干活// 场景1:有默认取消点(sleep是取消点)sleep(1); // 工人B暂停1秒(重点:这1秒里会“听命令”)printf("循环一次\n"); // 暂停结束后,说“我完成一次循环”// 场景2:无取消点(纯计算)→ 无法响应取消(坑!)// for(int i=0; i<1e8; i++); // 换成“疯狂算数字”:这时候工人B没空听命令}return NULL; // 理论上永远到不了这里(因为while(1)是死循环)
}关键逻辑:while(1):子线程会一直循环,不停干活。
sleep(1):不只是 “暂停 1 秒”,更重要的是它是一个 “取消点”—— 就像工人 B
在干活间隙喝口水,这时候会抬头看看有没有 “停下” 的命令。如果把sleep(1)换成纯计算(比如for循环算 1 亿次),工人 B 会一直埋头算,
没空看命令,就算收到 “停下” 的命令也不会理(这就是注释里说的 “坑”)。
3. 主线程函数(main):工人 A 的操作
c
运行
int main() {pthread_t tid; // 线程ID:相当于工人B的“工号”,用来标识这个线程// 第一步:创建子线程(让工人B开始干活)// 参数:tid(保存工号)、NULL(用默认设置)、worker(工人B要干的活)NULL(不给工人B传参数)pthread_create(&tid, NULL, worker, NULL); // 第二步:主线程等3秒(让工人B先干3秒)sleep(3); // 工人A说:“我先等3秒,让他多干一会儿”// 第三步:给子线程发“取消请求”(让工人B停下)printf("发送取消请求\n"); // 工人A喊:“工人B,停下!”pthread_cancel(tid); // 正式发命令(通过工号找到工人B)// 第四步:等待子线程退出(确认工人B真的停下了)pthread_join(tid, NULL); // 工人A等工人B停下,回收他的工具printf("线程已退出\n"); // 工人A确认:“工人B真的停下了”return 0; // 整个程序结束
}
关键逻辑:
pthread_create:创建线程的瞬间,worker函数就会开始运行(工人 B 立刻开始干活),和main函数(工人 A)并行执行(不是等main执行完再开始)。
sleep(3):这 3 秒里,main线程暂停,worker线程在疯狂循环:每 1 秒打印一次
“循环一次”,3 秒内会打印 3 次(第 1 秒、第 2 秒、第 3 秒各一次)。
pthread_cancel(tid):给worker线程发 “取消请求”,但不是 “强制断电”,而是
“发通知”—— 工人 B 收到通知后,会在 “取消点”(这里是sleep(1))的时候停下。pthread_join(tid, NULL):主线程必须等子线程退出,否则主线程先结束了,子线程
可能变成 “孤儿线程”(没人管的工人),导致资源泄漏。运行起来会发生什么?(全程推演)程序开始,main函数执行,调用pthread_create→worker线程启动,打印 “线程启动,
开始循环”。
worker进入while(1):
第一次sleep(1):暂停 1 秒(此时没收到取消命令,继续)→ 打印 “循环一次”。
第二次sleep(1):再暂停 1 秒→ 打印 “循环一次”(此时累计 2 秒)。
第三次sleep(1):开始暂停(此时main线程的sleep(3)刚好结束)。
main线程执行pthread_cancel(tid)→ 给worker发 “取消请求”。
worker在第三次sleep(1)的 “取消点” 检查到请求→ 立刻退出循环,终止线程。
main线程的pthread_join等待结束→ 打印 “线程已退出”→ 程序结束。
核心考点(嵌入式面试常问)
线程取消是 “协作式” 的:不是发命令就立刻停,必须等子线程到 “取消点”
(比如sleep、read等系统调用)才会停。
纯计算线程的坑:如果子线程一直在算数据(没有系统调用,无取消点),
就算发了pthread_cancel也不会停,必须用pthread_testcancel()手动加取消
点(下节课会讲)。pthread_join的作用:必须调用,否则子线程结束后资源(比如线程 ID、栈空间)不会被回收,导致内存泄漏。*:特殊技巧>>>>
段错误 segment的:运行错误用gbd调试
过程代码:
gcc -g -o pcancel pcancel.c -lpthread
linux@linux-VMware-Virtual-Platform:~/桌面/lv6_parall/subday5$ gdb ./pcancel
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./pcancel...
(gdb) run
Starting program: /home/linux/桌面/lv6_parall/subday5/pcancel This GDB supports auto-downloading debuginfo from the following URLs:<https://debuginfod.ubuntu.com>
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
Downloading separate debug info for system-supplied DSO at 0x7ffff7fc3000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff7bff6c0 (LWP 140547)]
main里创建好了,现在开始去等待7s,让func跑一下!!!
this is child thread
Downloading separate debug info for /lib/x86_64-linux-gnu/libgcc_s.so.1
[Thread 0x7ffff7bff6c0 (LWP 140547) exited] Thread 1 "pcancel" received signal SIGSEGV, Segmentation fault.
Download failed: 无效的参数. Continuing without source file ./string/../sysdeps/x86_64/multiarch/strlen-avx2.S.
__strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:512
warning: 512 ../sysdeps/x86_64/multiarch/strlen-avx2.S: 没有那个文件或目录
(gdb) bt
#0 __strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:512
#1 0x00007ffff7c6ada8 in __printf_buffer (buf=buf@entry=0x7fffffffda90, format=format@entry=0x555555556070 "thread retv is : %s !!!!>>>>><<<<<\n\n", ap=ap@entry=0x7fffffffdb90, mode_flags=mode_flags@entry=0) at ./stdio-common/vfprintf-process-arg.c:435
#2 0x00007ffff7c6b5a2 in __vfprintf_internal (s=0x7ffff7e045c0 <_IO_2_1_stdout_>, format=0x555555556070 "thread retv is : %s !!!!>>>>><<<<<\n\n", ap=ap@entry=0x7fffffffdb90, mode_flags=mode_flags@entry=0) at ./stdio-common/vfprintf-internal.c:1559
#3 0x00007ffff7c601b3 in __printf (format=<optimized out>) at ./stdio-common/printf.c:33
#4 0x00005555555552a6 in main (argc=1, args=0x7fffffffddc8) at pcancel.c:24
(gdb)
这是gdb调试的源过程:
到底发生了啥?改如何解决呢?
错误触发链(一句话总结)
plaintext
main函数第24行调用printf → printf调用__vfprintf_internal →__vfprintf_internal调用__printf_buffer → __printf_buffer调用__strlen_avx2计算字符串长度 →
因传入的retv是非法地址(PTHREAD_CANCELED),__strlen_avx2访问非法内存 → 触发段错误。如何根据调用栈解决问题?定位错误源码位置:从 #4 可知,错误的 “起点” 是main函数的pcancel.c:24行,直接查看该行代码:
必然是printf("thread retv is : %s ...", (char*)retv);—— 问题出在(char*)retv。分析参数合法性:结合线程取消的知识,retv在子线程被取消后是PTHREAD_CANCELED(非法地址),
不能当作char*字符串使用。修正代码:在printf前添加判断,区分 “正常返回” 和 “被取消”:
c
运行
if (retv == PTHREAD_CANCELED) {printf("线程被取消,无有效字符串返回\n");
} else {printf("thread retv is : %s ...", (char*)retv);
}
关键结论
调用栈的核心价值是 **“从错误发生的底层函数,回溯到用户代码中的触发点”**。这里的调用栈清晰告诉我们:
段错误的根源不是系统库函数(__strlen_avx2等)有问题,而是用户代码在main函数第 24 行
传入了非法的字符串指针。这也是嵌入式开发中定位内存错误的经典方法 ——永远相信调用栈指向的用户代码行。从main一步步到调用栈的最底层 vprintf -> printfbuffer -> strlen_avx2计算字符串长度
(9)设置允不允许被取消的状态!setcancelstate()
让他上一把锁!
一、函数原型与参数解析
c
运行
#include <pthread.h>
// 功能:设置线程的取消状态,返回0表示成功,非0表示失败
int pthread_setcancelstate(int state, // 新状态:PTHREAD_CANCEL_ENABLE(允许取消,默认)/ DISABLE(禁止取消)int *oldstate // 输出参数:保存原取消状态(可传NULL,即不保存)
);
state 取值:
PTHREAD_CANCEL_ENABLE:线程允许响应pthread_cancel请求(默认行为)。
PTHREAD_CANCEL_DISABLE:线程禁止响应pthread_cancel请求(关键操作时临时禁用)。
oldstate 作用:用于 “临时禁用后恢复原状态”,比如线程在关键操作前禁用取消,操作完成后通过oldstate恢复之前的状态。
二、核心作用:控制线程 “是否听指挥”
线程的 “取消状态” 决定了它是否会响应pthread_cancel发送的取消请求:
当state = PTHREAD_CANCEL_DISABLE时,即使其他线程调用pthread_cancel,该线程也会忽略请求,直到状态被改回ENABLE。
这一机制用于保护 **“原子性操作”**(如硬件驱动的寄存器写入、文件的原子性读写、嵌入式设备的初始化流程),避免中途被取消导致数据损坏或设备异常。
三、实战场景:嵌入式设备操作的 “原子性保障”
以串口设备读写为例,线程在 “打开串口→配置参数→写入数据” 的过程中,不允许被取消(否则会导致串口句柄泄漏、配置不完整),此时需用pthread_setcancelstate临时禁用取消:
c
运行
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>#define UART_DEV "/dev/ttyS3" // 嵌入式串口设备路径void *uart_task(void *arg) {int uart_fd = -1;int old_state;char data[] = "Embedded data transfer";// 步骤1:打开串口(非关键操作,允许取消)uart_fd = open(UART_DEV, O_RDWR);if (uart_fd == -1) {perror("open uart fail");pthread_exit(NULL);}// 步骤2:关键操作前,禁用取消(返回原状态到old_state)pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_state);// 步骤3:原子性操作:配置串口+写入数据(禁止被取消)// (实际场景中需添加串口配置代码,此处简化)write(uart_fd, data, strlen(data));// 步骤4:关键操作完成,恢复原取消状态pthread_setcancelstate(old_state, NULL);// 后续流程:允许被取消while (1) {sleep(1);printf("UART task running...\n");}return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, uart_task, NULL);sleep(5); // 运行5秒后尝试取消线程pthread_cancel(tid); // 发送取消请求pthread_join(tid, NULL); // 回收线程return 0;
}
效果:线程在 “写入串口数据” 时,即使主线程发送pthread_cancel,也会忽略请求,确保数据完整写入;操作完成后恢复原状态,后续可正常响应取消。
四、注意事项与易错点
“临时禁用” 的必要性:禁用取消是 “临时行为”,完成关键操作后必须恢复原状态(通过old_state),否则线程会永久忽略取消请求,导致资源泄漏。
错误处理:调用pthread_setcancelstate后需检查返回值(非 0 表示失败),例如:
c
运行
if (pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_state) != 0) {perror("pthread_setcancelstate fail");// 处理错误(如释放已申请资源)
}
与pthread_setcanceltype的区别:
pthread_setcancelstate:控制 “是否响应取消”(开关)。
pthread_setcanceltype:控制 “何时响应取消”(延迟响应DEFERRED/ 异步响应ASYNCHRONOUS)。
两者配合使用,可实现 “关键操作期间禁止取消,且仅在安全点响应取消” 的严格控制。(10)取消子线程的函数:
代码:为什么要成对出现pthread_cleanup_push + pop()
这段代码用于演示线程清理函数的触发机制,核心围绕pthread_cleanup_push/pop
(线程清理注册 / 注销)、pthread_cancel(线程取消)和pthread_join(线程回收)
展开,以下是分步解析:一、代码结构与功能模块
1. 清理函数 clean_func
c
运行
void clean_func(void* arg){printf(">>>>>>>> \n this is the clean function:%s \n<<<<<<<<<<<",(char*)arg);
}
作用:线程退出(尤其是被取消时)的资源释放 / 日志打印逻辑,参数arg是注册时传递的字符串,
用于验证清理函数的执行。2. 子线程函数 func
cvoid *func(void* arg){printf("this is child thread\n");pthread_cleanup_push(clean_func,"jibanao 123123test info \n");while(1){sleep(1);}pthread_exit("thread is now quitting!!!!\n");pthread_cleanup_pop(0);
}流程:
启动后打印子线程标识;
调用pthread_cleanup_push注册清理函数,传入字符串参数;
进入while(1)循环,每次sleep(1)(这是默认取消点—— 线程执行到此处会检查是否
有pthread_cancel请求);理论上的pthread_exit和pthread_cleanup_pop(0):pthread_cleanup_pop(0)表示
“弹出清理函数但不主动执行”,但线程被取消时,清理函数仍会自动执行。
3. 主线程函数 main
c
运行
int main(int argc,char* args[]){pthread_t tid ;void *retv;int i ;pthread_create(&tid,NULL,func,NULL);sleep(1);pthread_cancel(tid);pthread_join(tid,&retv);sleep(1);while(1){sleep(1);}return 0;
}
流程:
创建子线程tid,执行func;
休眠 1 秒,让子线程运行一段时间;
调用pthread_cancel(tid)向子线程发送取消请求;
调用pthread_join(tid, &retv)回收子线程资源;
主线程进入while(1)循环,程序持续运行。
二、运行效果与核心机制
子线程启动:打印"this is child thread",注册清理函数后进入sleep(1)循环。
主线程发送取消请求:pthread_cancel(tid)触发子线程在sleep(1)(取消点)处响应。清理函数触发:子线程被取消时,自动执行clean_func,打印传入的字符串
"jibanao 123123test info"。
线程回收:主线程通过pthread_join回收子线程资源,程序进入主线程的while(1)循环,持续运行。三、关键知识点与注意事项
pthread_cleanup_push/pop 成对性:必须在同一作用域(如同一函数、代码块)内成对出现,
否则编译报错。取消点的作用:sleep、read、pthread_cond_wait等是默认取消点,线程执行到这些点时会
检查取消请求;纯计算循环需手动添加pthread_testcancel()作为取消点。
pthread_cleanup_pop(nonzero) 参数:nonzero=1表示弹出时执行清理函数,nonzero=0表示
不执行;但线程被取消或调用pthread_exit时,无论参数是 0 还是 1,清理函数都会自动执行。这段代码是理解 “线程取消 + 清理函数” 的典型案例,核心价值在于展示线程被取消时,清理函
数如何自动执行以释放资源 / 打印日志,是嵌入式、服务器多线程开发中保障程序健壮性的关键技术。(11)如果变一下:
func里子进程的函数,不while1循环了!,主函数main里面不去cancel了,看看会不会cleanup一下?

结果:

照样能够执行并且退出!
也就是没有cancel照样能够退出执行!
其实:
pthread_exit执行,那么pthread_cleanup就会被执行!
** : #vip 如果pthread _ cleanup _pop(1),那就不会执行了,直接删掉了,退出了!
!!!又来一波反转!如果非0,但是又来一个sleep,也会执行退出函数!

比如:
现在不让他pop(0),加1哥sleep函数,一样可以出发clean函数!

plus:
pthread_cancel(pthread_self)也可以child子线程取消了自己!
具体代码:
要理解这个现象,需从 pthread_cancel 的工作机制 和 线程清理函数的触发逻辑
两个角度分析:
1. pthread_cancel 是 “异步请求”,非 “立即终止”
pthread_cancel(pthread_self()) 是发送 “取消请求”,
而非立即终止线程。线程只有在遇到 **“取消点”** 时,才会响应并终止。什么是取消点?取消点是线程检查并响应取消请求的时机,常见的取消点包括
:pthread_testcancel() 调用、部分标准库函数(如 read、sleep、pthread_exit 等)。
代码中 printf("should i exe?????\n") 不是取消点,因此线程会继续执行该打印语句,之后才会处理取消请求或执行 pthread_exit。2. pthread_exit 会触发清理函数,同时终止线程
即使线程发送了自我取消请求,后续调用 pthread_exit 时,线程会优先执行终止逻辑:执行 pthread_exit 时,会触发 pthread_cleanup_push 注册的清理函数(属于 “线程终止时触发清理函数” 的场景)。但在此之前,printf 已经执行完毕,所以会打印 “should i exe”。3. 代码中 pthread_cleanup_push/pop 配对错误(潜在风险)pthread_cleanup_push 和 pthread_cleanup_pop 必须严格配对,
且在同一个函数的同一词法作用域内。当前代码中,pthread_cleanup_pop 位于 pthread_exit 之后,这会导致配对逻辑错误,属于未定义行为(可能引发运行时异常或清理函数执行异常)。
正确的配对方式 应将 pthread_cleanup_pop 放在 pthread_exit 之前,例如:c
运行
void *func(void* arg){printf("this is child thread\n");pthread_cleanup_push(clean_func,"jibamao 123123test info \n");pthread_cancel(pthread_self());printf("should i exe?????\n");pthread_cleanup_pop(1); // 放在pthread_exit之前,完成配对pthread_exit("thread return value is here ");
}总结pthread_cancel 是 “异步请求”,线程在非取消点(如 printf)会继续执行,
因此 “should i exe” 会被打印。
pthread_exit 会触发清理函数,但 printf 已在其前执行完毕。
需修正 pthread_cleanup_push/pop 的配对逻辑,避免未定义行为。如何让他立即取消???:
解答:
设置一个PTHREAD_CANCEL_TYPE就可以知道这个里面的:pthread_setcanceltype的一个宏:asyncchromous就是让他同步的取消,立即取消!
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 示例清理函数:打印传入参数
void cleanup(void *arg) {printf("Cleanup function executed, arg: %s\n", (char *)arg);
}void cleanup2(void *arg) {printf("Cleanup2 function executed, arg: %s\n", (char *)arg);
}void *func(void *arg) {printf("This is child thread\n");pthread_cleanup_push(cleanup, "abcd");pthread_cleanup_push(cleanup2, "efgh");// while(1){sleep(1);}pthread_cancel(pthread_self());printf("Should not print\n");while(1) {printf("sleep\n");sleep(1);}pthread_exit("thread return");pthread_cleanup_pop(1);pthread_cleanup_pop(1);sleep(10);pthread_exit("thread return");
}int main() {pthread_t tid;pthread_create(&tid, NULL, func, NULL);pthread_join(tid, NULL);return 0;
}原因:
分析这段代码的执行结果,需结合线程清理函数的触发逻辑和**pthread_cancel的工作机制 ** 逐段拆解:
一、核心逻辑流程
线程启动与清理函数注册
子线程打印 This is child thread。
通过 pthread_cleanup_push 依次将 cleanup("abcd")、
cleanup2("efgh") 压入清理函数栈(栈结构:cleanup2 在上,cleanup 在下)。发送取消请求与中间执行
调用 pthread_cancel(pthread_self()) 发送 “自我取消请求”
(异步,需等待取消点响应)。
执行 printf("Should not print")(此时未到取消点,会正常打印)。进入 while(1) 循环,执行 printf("sleep\n") 后调用sleep(1) —— sleep 是 “取消点”,线程在此处响应取消请求。清理函数触发与线程终止
线程被取消时,清理函数栈中的函数会逆序执行(栈的 “先进后出” 特性):先执行 cleanup2("efgh"),再执行 cleanup("abcd")。线程终止,后续 pthread_exit、pthread_cleanup_pop、sleep(10)等代码全部跳过。二、预期输出示例(假设cleanup和cleanup2是打印参数的简单函数)
plaintext
This is child thread
Should not print
sleep
[cleanup2的输出,例如:"Cleanup2 executed with arg: efgh"]
[cleanup的输出,例如:"Cleanup executed with arg: abcd"]
三、代码规范性问题(需注意)
pthread_cleanup_push/pop 配对错误代码中 pthread_cleanup_pop位于 pthread_exit 之后,属于未配对调用,违反 POSIX 规范(会导致未定义行为)。正确做法是 push 和 pop 需在同一词法作用域内严格配对,且 pop 应在线程终止前执行。冗余逻辑pthread_exit("thread return")、pthread_cleanup_pop(1)、sleep(10)
等代码因线程被取消而永远不会执行,可视为冗余。总结代码的核心行为是 **“线程自我取消时,逆序执行清理函数栈中的函数”**,
输出包含线程启动提示、中间打印、取消点触发的清理函数输出。需注意清理函数的配对规范性,避免未定义行为。(12) day8:互斥、锁、临界区
(13)day9 :条件变量
(14) day10 线程池+gdb调试
(15)
