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_initpthread_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
警告: 分离后的线程不能再被 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_t 和内核线程 ID 都打出来,方便后期用 strace 或 GDB 定位问题。格式可以这样:[pthread:0x%lx][tid:%d]

3.4 线程退出:pthread_exit

线程退出有三种方式:

  1. 线程函数执行 return 返回
  2. 线程调用 pthread_exit
  3. 被其他线程调用 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 退出当前线程 注意指针生命周期
线程创建与管理核心流程 主线程 pthread_create pthread_attr_t 设置属性 分离线程 pthread_detach 自动回收资源 可接合线程 pthread_join 手动回收资源 pthread_exit / return pthread_exit / return

嗯,这张图把整个流程串起来了。你从主线程出发,设置属性,然后决定是分离还是可接合。分离线程自己玩自己的,资源自动回收;可接合线程需要你主动 join 去收尸。最后,不管哪种方式,线程退出时都可以用 pthread_exit 或 return 来结束。

我个人建议:如果你不确定线程是否需要被 join,那就默认用可接合模式。等确定不需要了,再改成分离。因为从可接合转分离很容易,反过来却不行。我曾经在项目里一开始全用分离,后来需要统计线程执行结果,改得那叫一个痛苦。


公众号:蓝海资料掘金营,微信deep3321