一、为什么需要线程?
进程的创建是为了提高并发,但进程是重量级的——创建和切换的开销很大。线程的出现,正是为了在保持并发能力的同时,降低系统开销。
1.1 进程与线程的本质区别
| 对比项 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定位 | 资源分配的最小单位 | CPU 调度的基本单位 |
| 侧重 | 资源分配单位 | 执行体、执行单位 |
| 别称 | 重量级进程 | 轻量级进程(LWP) |
| 组成 | PCB + [text | data | bss | 堆 | 栈] | 线程ID + 程序计数器(PC) + 一组相关寄存器 + 栈空间 |
| 地址空间 | 独立的虚拟地址空间 | 共享所属进程的地址空间 |
| 创建开销 | 大(需要复制页表、文件描述符表等) | 小(只需分配栈和少量管理结构) |
关键关系:
- 线程共享进程的资源(代码段、数据段、堆、文件描述符等)
- 线程存在于进程中,一个进程至少有一个线程
- 进程分为单线程进程和多线程进程
1.2 Linux 内核中的线程
Linux 早期并不原生支持线程,而是通过clone()系统调用来实现——它创建调用进程的一个拷贝,并与调用进程共享相同的地址空间。
重要事实:在 Linux 内核中,实际上并不严格区分进程和线程。
IBM 和 Red Hat 联合开发了NPTL(New/Native POSIX Thread Library)——这是 Linux 上的线程函数库,提供了标准的 POSIX 线程操作接口。我们后续使用的所有pthread_*系列函数都来自这个库。
二、线程的四大生命周期
线程操作与进程类似,分为四个阶段:
创建 → 执行 → 退出 → 资源回收2.1 线程创建 ——pthread_create
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);参数说明:
| 参数 | 含义 |
|---|---|
thread | 输出参数,用于接收新线程的 ID |
attr | 线程属性,传NULL表示使用默认属性(可结合、非分离) |
start_routine | 线程的执行函数——线程创建后从这个函数开始运行 |
arg | 传给线程执行函数的参数 |
返回值:成功返回0,失败返回错误码(注意不是 -1)。
示例:最简单的线程创建
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> // 线程执行函数 // 参数 arg 来自 pthread_create 的第四个参数 // 本例中为 NULL void *do_thread(void *arg) { // pthread_self() 获取当前线程自身的 ID printf("---do_thread ---- tid = %ld\n", pthread_self()); return NULL; // 线程函数返回,线程结束 } int main(int argc, const char *argv[]) { pthread_t tid; // 创建线程:属性为 NULL(默认),执行函数为 do_thread,参数为 NULL int ret = pthread_create(&tid, NULL, do_thread, NULL); if (ret != 0) // 非零表示创建失败 { errno = ret; // 将错误码设置到 errno perror("pthread_create fail"); return -1; } printf("pthread_create success\n"); printf("tid = %ld\n", tid); // 打印子线程 ID printf("main tid = %ld\n", pthread_self()); // 打印主线程 ID sleep(1); // 主线程等待 1 秒,确保子线程有时间执行 return 0; }注意:线程之间是对等关系,不存在进程间的父子关系。
2.2 线程执行 —— 传参与共享资源
线程共享进程空间,操作同一个资源时会相互影响。下面通过两个线程操作同一变量来展示:
示例:两个线程操作共享变量
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> // 线程1:每次将 var 加 1 void *do_thread1(void *arg) { int *var = arg; // 接收主线程传来的变量地址 while (1) { (*var)++; // 对共享变量做 +1 操作 printf("---do_thread1 ---- %d \n", *var); sleep(1); // 每秒操作一次 } return NULL; } // 线程2:每次将 var 加 2 void *do_thread2(void *arg) { int *var = arg; // 同样接收变量地址 while (1) { (*var) += 2; // 对共享变量做 +2 操作 printf("---do_thread2 ---- %d \n", *var); sleep(1); } return NULL; } int main(int argc, const char *argv[]) { int var = 0; // 共享变量 pthread_t tid[2]; // 创建线程1,将 &var 作为参数传入 int ret = pthread_create(&tid[0], NULL, do_thread1, &var); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } // 创建线程2,同样传入 &var ret = pthread_create(&tid[1], NULL, do_thread2, &var); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } printf("pthread_create success\n"); while (1) sleep(1); // 主线程无限等待,保持进程存活 return 0; }运行结果(示例):
---do_thread1 ---- 1 ---do_thread2 ---- 3 ---do_thread1 ---- 4 ---do_thread2 ---- 6 ---do_thread1 ---- 7 ...可以看到两个线程交替修改同一个变量var,这正是资源共享、相互影响的体现。在没有同步机制的情况下,这属于竞态条件(Race Condition),实际项目中需要用互斥锁等机制来保护。
2.3 线程退出
线程有四种退出方式:
| 方式 | 说明 |
|---|---|
1. 调用pthread_exit() | 主动退出,可携带退出状态值 |
2. 从线程执行函数中return | 等价于调用pthread_exit(return值) |
3. 被其他线程pthread_cancel() | 被动取消 |
4. 进程中任意线程调用exit()或 main 函数 return | 导致整个进程终止,所有线程都结束 |
pthread_exit函数
void pthread_exit(void *retval);功能:结束调用线程。参数retval是线程带出的退出状态值(一个指针)。
注意事项:
pthread_exit传递的是退出状态值对应空间的地址- 如果想获取该线程的退出状态值,需要由另一个线程通过
pthread_join来获取 - 如果在 main 线程中调用
pthread_exit(),只是结束了主线程的执行,并不会结束整个进程。进程会等待所有子线程执行结束后才销毁
示例:线程返回字符串
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> void *do_thread(void *arg) { static int ret = 10; while (1) { printf("---do_thread ---- tid = %ld\n", pthread_self()); } // 以下三种退出方式效果等价(在实际代码中只会用一种): // return NULL; // pthread_exit(&ret); return "i am dead...."; // 返回一个字符串指针作为退出状态值 } int main(int argc, const char *argv[]) { pthread_t tid; int ret = pthread_create(&tid, NULL, do_thread, NULL); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } printf("pthread_create success\n"); printf("tid = %ld\n", tid); printf("main tid = %ld\n", pthread_self()); #if 0 // 以下代码被条件编译禁用,但展示了如何获取线程返回值 void *retval = NULL; pthread_join(tid, &retval); // 等待子线程结束,获取退出值 // printf("retval = %d\n", *(int *)retval); // 如果返回的是 int 指针 printf("retval = %s\n", (char *)retval); // 返回的是字符串指针 #endif pthread_exit(NULL); // 主线程退出(但进程不会立即结束) return 0; }2.4 线程资源回收 ——pthread_join
int pthread_join(pthread_t thread, void **retval);| 参数 | 含义 |
|---|---|
thread | 要等待的线程的 tid |
retval | 输出参数,获取线程的退出状态值 |
返回值:成功返回0,失败返回错误码。
前提:线程必须是可结合属性(Joinable)——这是创建线程时的默认属性。
示例:无人机多线程模型(控制、视频、传输、存储)
这个示例模拟了一个无人机系统,包含 4 个子线程分别负责控制、视频、传输和存储功能:
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> // 定义线程函数指针类型,方便批量管理 typedef void *(*threadFunc_t)(void *); // 控制线程:运行 5 秒 void *do_ctrl(void *arg) { static int ret = 1; // static 变量确保函数返回后地址仍有效 int n = 5; while (n) { printf("-----%s----\n", __func__); // 打印函数名 "do_ctrl" sleep(1); --n; } return &ret; // 返回退出状态值的地址 } // 视频线程:运行 4 秒 void *do_video(void *arg) { static int ret = 2; int n = 4; while (n) { printf("-----%s----\n", __func__); sleep(1); --n; } return &ret; } // 传输线程:运行 3 秒 void *do_trans(void *arg) { static int ret = 3; int n = 3; while (n) { printf("-----%s----\n", __func__); sleep(1); --n; } return &ret; } // 存储线程:运行 2 秒 void *do_store(void *arg) { static int ret = 4; int n = 2; while (n) { printf("-----%s----\n", __func__); sleep(1); --n; } return &ret; } int main(int argc, const char *argv[]) { pthread_t tid[4]; // 用函数指针数组统一管理 4 个线程函数 threadFunc_t tfunc[4] = {do_ctrl, do_video, do_trans, do_store}; int i = 0; // 批量创建 4 个线程 for (i = 0; i < 4; ++i) { int ret = pthread_create(&tid[i], NULL, tfunc[i], NULL); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } } // 批量回收 4 个线程,并获取各自的退出状态值 for (i = 0; i < 4; ++i) { int *retval = NULL; // 阻塞等待第 i 个线程结束,并将退出值地址存入 retval pthread_join(tid[i], (void **)&retval); // 通过指针解引用获取实际的返回值 printf("ret = %d \n", *retval); } return 0; }运行结果(示例):
-----do_ctrl---- -----do_video---- -----do_trans---- -----do_store---- -----do_ctrl---- -----do_video---- -----do_trans---- -----do_store---- -----do_ctrl---- -----do_video---- -----do_ctrl---- -----do_ctrl---- ret = 1 ret = 2 ret = 3 ret = 4要点:返回值指针必须指向
static变量或全局变量/堆内存,不能指向线程栈上的局部变量——否则线程退出后栈被销毁,指针变成悬垂指针。
三、线程属性与分离设置
3.1 两种线程属性
| 属性 | 说明 |
|---|---|
| 可结合(Joinable,默认) | 线程结束后需要通过pthread_join回收资源 |
| 分离(Detached) | 线程结束后系统自动回收资源,无需也不能pthread_join |
3.2 设置分离属性的两种方式
方式一:通过属性对象(创建时设置)
pthread_attr_t attr; pthread_attr_init(&attr); // 初始化属性对象 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离属性 pthread_create(&tid, &attr, do_thread, NULL); // 用该属性创建线程 pthread_attr_destroy(&attr); // 销毁属性对象方式二:创建后动态设置(使用pthread_detach)
pthread_create(&tid, NULL, do_thread, NULL); // 默认可结合属性 pthread_detach(tid); // 创建后标记为分离状态示例:使用pthread_detach设置分离属性
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> void *do_thread(void *arg) { printf("---do_thread ---- tid = %ld\n", pthread_self()); return NULL; } int main(int argc, const char *argv[]) { // 以下是被注释的"方式一"代码: // pthread_attr_t attr; // pthread_attr_init(&attr); // pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // int ret = pthread_create(&tid, &attr, do_thread, NULL); pthread_t tid; // 使用默认属性(可结合)创建线程 int ret = pthread_create(&tid, NULL, do_thread, NULL); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_detach(tid); // 创建后将线程标记为分离状态 // pthread_attr_destroy(&attr); // 方式一需要销毁属性对象 printf("pthread_create success\n"); printf("tid = %ld\n", tid); printf("main tid = %ld\n", pthread_self()); sleep(1); // 对已分离的线程调用 pthread_join 会失败 if ((ret = pthread_join(tid, NULL)) != 0) { errno = ret; perror("pthread_join fail"); // 会打印此错误:Invalid argument return -1; } return 0; }运行输出:
---do_thread ---- tid = xxxxx pthread_create success tid = xxxxx main tid = xxxxx pthread_join fail: Invalid argument关键点:已经被
pthread_detach的线程,再调用pthread_join会失败返回EINVAL。
3.3 子线程回收策略
| 场景 | 策略 |
|---|---|
| 子线程能在有限时间内结束 | 使用pthread_join正常等待回收 |
| 子线程可能休眠或阻塞 | 等待一定时间后强制回收 |
| 子线程必须长时间运行 | 设置为分离属性,不再回收 |
四、线程取消 ——pthread_cancel
int pthread_cancel(pthread_t th);功能:向目标线程发送取消请求。线程之间可以相互取消。
示例:子线程取消主线程
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> pthread_t mtid = 0; // 全局变量,保存主线程 ID void *do_thread(void *arg) { printf("---do_thread ---- tid = %ld\n", pthread_self()); sleep(1); pthread_cancel(mtid); // 子线程取消主线程 return NULL; } void cleanup(void *arg) { printf("cleanup ----\n"); // 线程清理函数 } int main(int argc, const char *argv[]) { pthread_t tid; mtid = pthread_self(); // 记录主线程自身的 ID int ret = pthread_create(&tid, NULL, do_thread, NULL); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } // 注册清理函数:当主线程被取消时会触发 pthread_cleanup_push(cleanup, NULL); printf("pthread_create success\n"); printf("tid = %ld\n", tid); printf("main tid = %ld\n", pthread_self()); sleep(5); // 主线程睡眠 5 秒(但 1 秒后就会被子线程取消) // pthread_exit(NULL); while (1) sleep(1); pthread_cleanup_pop(0); // 正常流程中弹出(不执行) return 0; }运行结果:
---do_thread ---- tid = xxxxx pthread_create success tid = xxxxx main tid = xxxxx cleanup ---- ← 主线程被取消时,触发了清理函数注意:
pthread_cleanup_pop(0)中参数为 0 表示弹出但不执行清理函数。但因为主线程在sleep(5)期间被子线程取消了,清理函数会由取消机制自动触发执行。
五、线程清理函数 ——pthread_cleanup_push / pop
这是线程版的"退出清理"机制,类似于进程中的atexit()。
5.1 函数原型
c
// 注册清理函数 void pthread_cleanup_push(void (*routine)(void *), void *arg); // 弹出清理函数 void pthread_cleanup_pop(int execute);| 参数 | 含义 |
|---|---|
routine | 清理函数的入口地址 |
arg | 传给清理函数的参数 |
execute | 非零值:弹出并执行清理函数;0:弹出但不执行 |
5.2 触发方式
清理函数在以下三种情况下会被自动触发:
| 触发条件 | 说明 |
|---|---|
pthread_cleanup_pop(非零值) | 主动弹出并执行 |
pthread_exit()被调用 | 线程退出时自动触发 |
pthread_cancel()被调用 | 线程被取消时自动触发 |
5.3 实际代码中的使用模式
pthread_cleanup_push(cleanup, NULL); // 注册清理函数 // ... 线程的工作代码 ... // 如果这里发生 pthread_cancel 或 pthread_exit, // cleanup 会被自动调用 pthread_cleanup_pop(0); // 正常结束,弹出但不执行示例:pthread_cleanup_push/pop与pthread_cancel配合
#include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> // 线程执行函数 void *do_thread(void *arg) { printf("---do_thread ---- tid = %ld\n", pthread_self()); return NULL; } // 线程清理函数——当线程被取消或调用 pthread_exit 时执行 void cleanup(void *arg) { printf("cleanup ----\n"); } int main(int argc, const char *argv[]) { pthread_t tid; int ret = pthread_create(&tid, NULL, do_thread, NULL); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } // 在主线程中注册清理函数 pthread_cleanup_push(cleanup, NULL); printf("pthread_create success\n"); printf("tid = %ld\n", tid); printf("main tid = %ld\n", pthread_self()); sleep(1); // 等待子线程执行 // pthread_cleanup_pop(1); ← 如果参数为 1,会直接执行 cleanup return 0; // 主线程正常返回 }展开原理:
pthread_cleanup_push和pthread_cleanup_pop在底层实际上是宏,它们内部使用do { ... } while(0)结构配合setjmp/longjmp来实现清理函数的注册和调用。因此它们必须成对使用,不能缺少任何一个。
六、综合实战:线程计算器
需求:从主线程获取用户输入的表达式(如3.5*2.0),组织成结构体传给子线程,子线程计算后将结果存回结构体,主线程输出结果。
#include <stdio.h> #include <pthread.h> #include <errno.h> // 定义计算结构体 typedef struct { float a; // 第一个操作数 float b; // 第二个操作数 char c; // 运算符 (+, -, *, /) float d; // 计算结果 } cal_t; // 加法线程 void *do_add(void *arg) { cal_t *p = arg; // 接收结构体指针 p->d = p->a + p->b; // 计算结果直接存回结构体 return NULL; } // 减法线程 void *do_sub(void *arg) { cal_t *p = arg; p->d = p->a - p->b; return NULL; } // 乘法线程 void *do_mul(void *arg) { cal_t *p = arg; p->d = p->a * p->b; return NULL; } // 除法线程 void *do_div(void *arg) { cal_t *p = arg; p->d = p->a / p->b; // 注意:未处理除零错误 return NULL; } int main(int argc, const char *argv[]) { cal_t s; printf("Input a pression:"); // 格式化输入:如 "3.5*2.0" 会被解析为 a=3.5, c='*', b=2.0 scanf("%f%c%f", &s.a, &s.c, &s.b); pthread_t tid; int ret = 0; // 根据运算符创建对应的计算线程 switch (s.c) { case '+': ret = pthread_create(&tid, NULL, do_add, &s); // 传入结构体地址 if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); // 等待计算完成 printf("result = %.2f\n", s.d); // 从结构体读取结果 break; case '-': ret = pthread_create(&tid, NULL, do_sub, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; case '*': ret = pthread_create(&tid, NULL, do_mul, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; case '/': ret = pthread_create(&tid, NULL, do_div, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; } pthread_exit(NULL); // 主线程退出 return 0; }#include <stdio.h> #include <pthread.h> #include <errno.h> // 定义计算结构体 typedef struct { float a; // 第一个操作数 float b; // 第二个操作数 char c; // 运算符 (+, -, *, /) float d; // 计算结果 } cal_t; // 加法线程 void *do_add(void *arg) { cal_t *p = arg; // 接收结构体指针 p->d = p->a + p->b; // 计算结果直接存回结构体 return NULL; } // 减法线程 void *do_sub(void *arg) { cal_t *p = arg; p->d = p->a - p->b; return NULL; } // 乘法线程 void *do_mul(void *arg) { cal_t *p = arg; p->d = p->a * p->b; return NULL; } // 除法线程 void *do_div(void *arg) { cal_t *p = arg; p->d = p->a / p->b; // 注意:未处理除零错误 return NULL; } int main(int argc, const char *argv[]) { cal_t s; printf("Input a pression:"); // 格式化输入:如 "3.5*2.0" 会被解析为 a=3.5, c='*', b=2.0 scanf("%f%c%f", &s.a, &s.c, &s.b); pthread_t tid; int ret = 0; // 根据运算符创建对应的计算线程 switch (s.c) { case '+': ret = pthread_create(&tid, NULL, do_add, &s); // 传入结构体地址 if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); // 等待计算完成 printf("result = %.2f\n", s.d); // 从结构体读取结果 break; case '-': ret = pthread_create(&tid, NULL, do_sub, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; case '*': ret = pthread_create(&tid, NULL, do_mul, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; case '/': ret = pthread_create(&tid, NULL, do_div, &s); if (ret != 0) { errno = ret; perror("pthread_create fail"); return -1; } pthread_join(tid, NULL); printf("result = %.2f\n", s.d); break; } pthread_exit(NULL); // 主线程退出 return 0; }运行示例:
text
Input a pression:3.5*2.0 result = 7.00Input a pression:3.5*2.0 result = 7.00设计亮点:利用结构体指针实现主线程和子线程之间的数据传递——主线程负责输入和输出,子线程负责计算。通过
pthread_join确保计算完成后才读取结果。
七、进程 vs 线程:性能对比
7.1 fork 性能测试(创建 10000 个子进程)
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(int argc, const char *argv[]) { int i = 10000; pid_t pid; for (i = 10000; i >= 0; --i) { pid = fork(); // 创建子进程(开销大:复制页表等) if (pid > 0) // 父进程 { wait(NULL); // 等待子进程结束(回收资源) continue; } else if (pid == 0) // 子进程 { exit(100); // 立即退出 } } return 0; }7.2 线程性能测试(创建 10000 个线程)
#include <stdio.h> #include <pthread.h> // 线程函数:直接退出 void *thread(void *arg) { pthread_exit(NULL); } int main(int argc, const char *argv[]) { pthread_t tid; int i = 10000; for (; i >= 0; --i) { // 创建线程(开销小:只需分配栈) int ret = pthread_create(&tid, NULL, thread, NULL); pthread_join(tid, NULL); // 等待并回收 } return 0; }结论:同样创建 10001 个执行体,线程版本的执行速度远快于进程版本。因为fork需要复制整个进程的地址空间、页表、文件描述符表等,而pthread_create只需分配少量栈空间和管理结构。
编译线程程序时需要链接 pthread 库:
gcc -o test test.c -lpthread
八、查看线程的 Linux 命令
# 查看所有线程(LWP = Light Weight Process) ps -eLf # 更详细地查看线程信息 ps -eLo pid,ppid,lwp,stat,comm输出中的LWP列就是线程 ID,同一进程的多个线程共享相同的PID。
九、总结速查表
9.1 线程 API 速查
| 函数 | 功能 | 对应进程函数 |
|---|---|---|
pthread_create | 创建线程 | fork |
pthread_exit | 退出线程 | exit |
pthread_join | 等待线程结束并回收资源 | wait |
pthread_cancel | 取消线程 | kill |
pthread_detach | 设置线程为分离状态 | — |
pthread_self | 获取当前线程 ID | getpid |
pthread_cleanup_push | 注册清理函数 | atexit |
pthread_cleanup_pop | 弹出(并可选执行)清理函数 | — |
pthread_attr_init | 初始化属性对象 | — |
pthread_attr_destroy | 销毁属性对象 | — |
pthread_attr_setdetachstate | 设置分离属性 | — |
9.2 线程退出方式总结
线程结束的 4 种方式: ├── 1. pthread_exit(retval) ← 主动退出,可携带状态值 ├── 2. return retval ← 从执行函数返回 ├── 3. pthread_cancel(tid) ← 被其他线程取消 └── 4. exit() / main() return ← 整个进程终止(所有线程都结束)9.3 清理函数触发条件
pthread_cleanup_push 注册的清理函数在以下情况触发: ├── 1. pthread_cleanup_pop(非零值) ← 主动触发 ├── 2. 线程调用 pthread_exit() ← 退出时触发 └── 3. 线程被 pthread_cancel() ← 被取消时触发十、注意事项与常见陷阱
- 1.
线程共享资源需同步保护:多线程操作同一变量时,需要使用互斥锁(
pthread_mutex)等机制,否则会出现竞态条件。 - 2.
pthread_exit不等于进程退出:在 main 函数中调用pthread_exit()只是结束主线程,进程会等所有子线程结束后才退出。 - 3.
返回值指针的生命周期:线程函数返回的指针不能指向栈上的局部变量,应该指向
static变量、全局变量或堆内存。 - 4.
pthread_cleanup_push/pop必须配对:它们是宏实现,必须在同一代码块中成对出现,否则编译报错。 - 5.
分离后的线程不能再
pthread_join:对已分离的线程调用pthread_join会返回EINVAL。 - 6.
编译时别忘了
-lpthread:使用 POSIX 线程函数库需要链接 pthread 库。