4. 线程的生命周期管理:从创建到消亡

线程的生命周期,说白了就是一条命从生到死的过程。我做了这么多年系统编程,见过太多因为生命周期管理不当导致的诡异bug。今天咱们就把这事彻底聊透。

4.1 线程的五种状态与转换图

一个线程在它的生命周期里,会经历五种核心状态。我习惯用一张图来理解它们的关系:

新建 就绪 运行 阻塞 等待 终止 pthread_create 调度 时间片耗尽/抢占 I/O请求 I/O完成 pthread_join/wait 条件满足 return/exit

这张图我画了很多遍,每次给团队新人讲线程管理都会拿出来。你仔细看,核心就三条线:

  • 新建 → 就绪:线程创建后不会立刻跑,得等调度器点头
  • 就绪 ↔ 运行:这是调度器的主战场,来回切换
  • 运行 → 阻塞/等待 → 就绪:遇到I/O或同步操作,线程就得让出CPU

4.2 用ps和top命令查看线程状态

光讲理论不行,得能动手看。我平时排查问题,最常用的就是这两个命令。

4.2.1 ps命令查看线程

加个 -T 参数就能看到线程了:

# 查看进程1234的所有线程
ps -T -p 1234

# 更详细的输出,显示状态
ps -eLf | grep myapp

输出里有个 STAT 列,那玩意就是线程状态。常见的状态码:

状态码 含义 对应生命周期
R Running/Runnable 运行或就绪
S Sleeping(可中断) 阻塞
D Uninterruptible Sleep 阻塞(不可中断)
T Stopped 暂停
Z Zombie 僵尸

我的小技巧:看到大量 D 状态的线程,八成是磁盘I/O出问题了。我曾经在生产环境遇到过,一堆线程卡在 D 状态,最后发现是存储阵列挂了。

4.2.2 top命令查看线程

top 默认只看进程,想看线程得加 -H:

# 查看所有线程
top -H

# 只看某个进程的线程
top -H -p 1234

top 里有个 S 列,和 ps 的 STAT 是一个意思。我一般这么用:

  • 按 P 键按CPU排序,找最耗CPU的线程
  • 按 M 键按内存排序,找内存大户
  • 看 S 列,如果全是 R 但CPU不高,可能是死循环

注意:top 看到的线程ID(TID)和 ps 里的 LWP 是一个东西。别和进程ID搞混了,我见过有人拿TID当PID去kill,结果把整个进程干掉了。

4.3 僵尸线程与孤儿线程

这两个概念,说白了都是线程退出后的遗留问题。处理不好,系统资源就泄漏了。

4.3.1 僵尸线程

线程执行完了,但它的退出状态还没被回收。这时候线程就变成了僵尸。你看 ps 输出,状态是 Z。

为什么会这样?因为线程退出后,内核还保留着它的退出码、资源使用统计等信息。得有人来取走这些信息,线程才能真正消亡。

处理方式就一个:pthread_join。

#include <pthread.h>
#include <stdio.h>

void* worker(void* arg) {
    printf("线程工作中...\n");
    return (void*)42;  // 退出码
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, worker, NULL);
    
    // 关键:等待线程结束,回收资源
    void* retval;
    pthread_join(tid, &retval);
    printf("线程退出码: %ld\n", (long)retval);
    
    return 0;
}

避坑指南:我曾经写过一个服务,创建了大量线程但忘了join。结果跑了一周后,ps一看全是Z状态。系统负载不高,但线程数暴涨,最后把进程数上限打满了。从那以后,我每个pthread_create后面都紧跟着pthread_join,或者用分离线程。

4.3.2 孤儿线程

主线程退出了,但子线程还在跑。这时候子线程就成了孤儿。在Linux下,孤儿线程会被init进程(PID=1)收养。

处理方式有两种:

  1. 分离线程:用 pthread_detach 让线程自己玩,退出后自动回收
  2. 等待所有线程:主线程退出前,join所有子线程
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* detached_worker(void* arg) {
    printf("分离线程开始工作...\n");
    sleep(2);
    printf("分离线程结束\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, detached_worker, NULL);
    
    // 设置为分离状态
    pthread_detach(tid);
    
    // 主线程可以安全退出了
    printf("主线程退出\n");
    pthread_exit(NULL);  // 注意:用exit()会干掉所有线程
}

重要:主线程退出时,如果用 exit() 或从 main 里 return,整个进程就结束了,所有线程都会被强制终止。想只退出主线程,用 pthread_exit()。

4.4 实际项目中的经验总结

说了这么多,我总结几条实战经验:

  • 线程池是王道:别频繁创建销毁线程,用线程池管理。我见过一个项目每秒创建1000个线程,系统直接崩了。
  • 分离还是join:如果不需要线程的返回值,就用分离线程。需要返回值,必须join。
  • 监控线程状态:写个脚本定期ps -eLf,统计各状态线程数。一旦Z状态超过阈值,立刻报警。
  • 资源清理:线程退出前,确保释放了所有资源。锁、内存、文件描述符,一个都不能漏。

嗯,线程生命周期这块,说白了就是管好线程的出生、活着、死亡三个阶段。出生要优雅,活着要监控,死亡要善后。做到这三点,线程管理就稳了。

可可精品资料库