3. 线程的创建与管理:线程属性、分离线程、线程ID与退出
好,我们继续往下走。上一章我们把线程创建的基础讲透了,但说实话,实际工程里光会 pthread_create 是远远不够的。你想想看,线程创建出来之后,它的“性格”是什么样的?是跑完就自己消失,还是得等着别人来收尸?它的栈空间多大?调度策略是啥?
这些问题,都跟线程属性有关。我个人习惯是,只要项目里线程数量超过 3 个,就一定会用到 pthread_attr_t。为什么?因为默认属性太“糙”了,很多场景下根本扛不住。
3.1 线程属性:pthread_attr_t
线程属性说白了就是一个结构体,用来描述线程的“出厂设置”。你可以在创建线程之前,先把这个结构体初始化,然后往里填各种参数。
#include <pthread.h>
#include <stdio.h>
int main() {
pthread_attr_t attr;
pthread_t tid;
// 初始化属性对象
pthread_attr_init(&attr);
// 设置线程为分离状态(后面会细讲)
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置栈大小(单位:字节)
pthread_attr_setstacksize(&attr, 1024 * 1024); // 1MB
// 创建线程
pthread_create(&tid, &attr, thread_func, NULL);
// 销毁属性对象
pthread_attr_destroy(&attr);
// ... 其他代码
return 0;
}
嗯,这里要注意:pthread_attr_init 和 pthread_attr_destroy 必须成对出现。我在项目中遇到过有人只 init 不 destroy,结果内存泄漏查了半天。虽然这个泄漏量不大,但积少成多,尤其是在嵌入式环境里,很要命。
detachstate:线程分离状态stacksize:线程栈大小schedpolicy:调度策略(SCHED_FIFO、SCHED_RR 等)schedparam:调度优先级inheritsched:是否继承调用线程的调度属性
3.2 分离线程:pthread_detach
线程创建后,默认是“可接合”的(joinable)。什么意思?就是说主线程必须调用 pthread_join 来等待它结束,并回收它的资源。如果你不 join,线程结束后资源不会被释放,这就造成了线程泄漏。
但有些线程,比如后台日志线程、监控线程,你根本不需要等它结束。这时候就该用分离线程了。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void* worker(void* arg) {
printf("子线程开始工作...\n");
sleep(2);
printf("子线程结束\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, worker, NULL);
pthread_attr_destroy(&attr);
// 主线程不需要 join,直接继续
printf("主线程继续做自己的事\n");
sleep(3);
printf("主线程结束\n");
return 0;
}
或者,你也可以在线程创建后,再调用 pthread_detach 把它分离:
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid); // 分离,不再需要 join
pthread_join,会返回 ESRCH 错误。我曾经在代码审查时看到有人先 detach 再 join,结果线程资源一直没回收,内存越涨越高。
3.3 线程ID获取
每个线程都有两个 ID:一个是 pthread_t 类型的线程标识符(由 pthread 库管理),另一个是操作系统层面的内核线程 ID(LWP,轻量级进程)。
获取 pthread_t 很简单:
pthread_t self_id = pthread_self();
printf("当前线程的 pthread ID: %lu\n", (unsigned long)self_id);
获取内核线程 ID 需要调用系统调用:
#include <sys/syscall.h>
#include <unistd.h>
pid_t tid = syscall(SYS_gettid);
printf("内核线程 ID: %d\n", tid);
为什么要区分这两个 ID?我在调试一个多线程死锁问题时,用 GDB 看 info threads 显示的是内核线程 ID,而代码里打印的是 pthread_t,两者对不上,排查了半天。后来才意识到需要把两个 ID 都打印出来做映射。
[pthread:0x%lx][tid:%d]
3.4 线程退出:pthread_exit
线程退出有三种方式:
- 线程函数执行 return 返回
- 线程调用
pthread_exit - 被其他线程调用
pthread_cancel取消
pthread_exit 和 return 最大的区别在于:return 只会退出当前线程函数,而 pthread_exit 可以在线程函数内部的任何位置退出,包括嵌套函数中。
#include <pthread.h>
#include <stdio.h>
void* sub_task(void* arg) {
printf("子任务执行中...\n");
// 直接退出,不返回给调用者
pthread_exit((void*)42);
}
void* worker(void* arg) {
printf("工作线程启动\n");
sub_task(NULL);
// 这行不会执行到
printf("这行不会打印\n");
return NULL;
}
int main() {
pthread_t tid;
void* retval;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, &retval);
printf("子线程退出码: %ld\n", (long)retval);
return 0;
}
这里有个坑:pthread_exit 传入的指针必须指向全局变量或堆内存,不能是局部变量。因为线程退出后,栈空间会被回收,局部变量就失效了。
| 函数 | 作用 | 注意事项 |
|---|---|---|
| pthread_attr_init | 初始化线程属性对象 | 必须配对 destroy |
| pthread_attr_setdetachstate | 设置线程分离状态 | 分离后不可 join |
| pthread_self | 获取当前线程的 pthread_t | 用于日志和调试 |
| pthread_detach | 将线程设为分离状态 | 调用后资源自动回收 |
| pthread_exit | 退出当前线程 | 注意指针生命周期 |
嗯,这张图把整个流程串起来了。你从主线程出发,设置属性,然后决定是分离还是可接合。分离线程自己玩自己的,资源自动回收;可接合线程需要你主动 join 去收尸。最后,不管哪种方式,线程退出时都可以用 pthread_exit 或 return 来结束。
我个人建议:如果你不确定线程是否需要被 join,那就默认用可接合模式。等确定不需要了,再改成分离。因为从可接合转分离很容易,反过来却不行。我曾经在项目里一开始全用分离,后来需要统计线程执行结果,改得那叫一个痛苦。