一、问题:线程结束就自动销毁了吗?
1.1 一个容易被忽略的事实
很多初学者认为:线程函数执行完 return 后,线程就"消失"了,所有资源都被释放。这是错误的!
实际上,Linux 线程结束后,它的线程状态 和部分资源 (如线程栈)并不会自动释放,而是处于一种类似"僵尸线程"的状态。如果不主动回收,这些资源会一直占用内存,导致资源泄漏。
1.2 类比:进程需要 wait,线程也需要 join
| 进程 | 线程 |
|---|---|
| 子进程结束变成僵尸进程 | 线程结束变成僵尸线程 |
父进程需要 wait() 回收 |
主线程需要 pthread_join() 回收 |
或者设置 signal(SIGCHLD, SIG_IGN) 自动回收 |
或者调用 pthread_detach() 自动回收 |
核心结论:线程结束 ≠ 线程资源被释放,必须主动回收!
二、线程销毁的两种方法
2.1 方法一:pthread_join(阻塞式等待)
c
int pthread_join(pthread_t thread, void **retval);
- 作用:等待指定线程结束,并回收其资源。
- 特点 :调用线程会阻塞,直到目标线程终止。
- 优点:可以获取线程的返回值。
- 缺点:如果线程一直不结束,调用者会一直等。
适用场景:需要知道线程何时结束,或需要获取返回值时。
2.2 方法二:pthread_detach(非阻塞分离)
c
int pthread_detach(pthread_t thread);
- 作用 :将线程标记为"分离状态",线程结束后自动回收资源。
- 特点 :调用后立即返回,不阻塞。
- 优点 :不需要
pthread_join,线程结束自动清理。 - 缺点 :无法获取线程的返回值,也不能再
join。
适用场景:不需要关心线程何时结束,也不需要返回值的"后台任务"。
三、代码示例
3.1 使用 pthread_join
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("子线程:开始工作...\n");
sleep(2);
printf("子线程:工作完成\n");
return (void*)42; // 返回一个值
}
int main() {
pthread_t tid;
void *ret;
// 创建线程
pthread_create(&tid, NULL, thread_func, NULL);
// 等待线程结束,并获取返回值
pthread_join(tid, &ret);
printf("主线程:子线程返回值 = %ld\n", (long)ret);
return 0;
}
输出:
子线程:开始工作...
子线程:工作完成
主线程:子线程返回值 = 42
说明:
- 主线程在
pthread_join处阻塞,直到子线程结束。 - 子线程的返回值通过
ret获取。 - 子线程资源被自动回收。
3.2 使用 pthread_detach
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
// 分离自身(常用写法)
pthread_detach(pthread_self());
printf("子线程:开始工作...\n");
sleep(2);
printf("子线程:工作完成,自动销毁\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 主线程继续做其他事情,不等子线程
printf("主线程:继续执行...\n");
sleep(3); // 等待子线程完成(否则主线程结束进程会终止子线程)
printf("主线程:结束\n");
return 0;
}
输出:
主线程:继续执行...
子线程:开始工作...
子线程:工作完成,自动销毁
主线程:结束
说明:
- 子线程调用
pthread_detach(pthread_self())将自己设为分离状态。 - 主线程不需要
pthread_join,子线程结束后资源自动回收。 - 主线程必须"活得够久",否则进程结束会强制终止所有线程。
3.3 在创建时直接设置分离属性
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_func(void* arg) {
printf("子线程:执行中...\n");
sleep(1);
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr;
// 1. 初始化线程属性
pthread_attr_init(&attr);
// 2. 设置分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 3. 创建分离线程
pthread_create(&tid, &attr, thread_func, NULL);
// 4. 销毁属性对象(不再需要)
pthread_attr_destroy(&attr);
printf("主线程:继续执行...\n");
sleep(2); // 等子线程完成
return 0;
}
说明 :这种方式更优雅,子线程从一开始就是分离的,无需在函数内部调用 pthread_detach。
四、常见错误与注意事项
4.1 既 join 又 detach
c
pthread_detach(tid); // 设为分离
pthread_join(tid, NULL); // 错误!分离线程不能再 join
分离线程不能再被 join,否则 pthread_join 会返回错误(通常为 EINVAL)。
4.2 主线程提前退出
c
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 没有 join,也没有 sleep
return 0; // 进程结束,所有线程被强制终止
}
后果 :子线程可能还没执行完就被强行终止。解决方法是调用 pthread_join 等待,或让主线程 sleep 足够长时间(不推荐)。
4.3 忘记回收导致内存泄漏
c
void* worker(void* arg) {
// 工作...
return NULL;
}
int main() {
while (1) {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
// 没有 join,也没有 detach
// 每次循环都会泄漏一个线程的资源
}
}
后果 :长时间运行后,内存被僵尸线程耗尽。必须使用 pthread_join 或 pthread_detach。
4.4 分离后仍想同步
分离线程无法被 join,如果主线程需要知道子线程的完成时机,就不能分离。可以使用条件变量 或全局标志来同步,但会增加复杂度。
五、两种方法的对比总结
| 特性 | pthread_join | pthread_detach |
|---|---|---|
| 是否阻塞 | ✅ 阻塞 | ❌ 不阻塞 |
| 获取返回值 | ✅ 可以 | ❌ 不可以 |
| 能否再次回收 | 只能一次 | 分离后不能再操作 |
| 适用场景 | 需要等待结果 | 后台任务,无需结果 |
| 资源回收 | 调用时回收 | 线程结束时自动回收 |
| 主线程需等待 | 自动同步 | 需额外同步(如 sleep) |
六、记忆口诀
线程结束不销毁,就像僵尸满地跑。
主动回收有两种:join 和 detach。
join 阻塞等结果,返回值也能拿到。
detach 分离不阻塞,自动回收省烦恼。
分离不能再 join,主线程别先溜掉。
资源回收要记牢,内存泄漏不能要。
七、一句话总结
线程结束 ≠ 资源释放。要么用
pthread_join主动等待回收(阻塞),要么用pthread_detach让系统自动回收(非阻塞)。二选一,绝不遗漏,否则僵尸线程会吃光你的内存!