线程等待、终止与资源回收
目录
一、线程等待
1、为什么需要线程等待?
1. 资源回收
2. 避免僵尸线程
3. 同步线程执行
4. 获取线程返回值
2、pthread_join 函数
3、线程终止状态与 pthread_join
4、演示三种线程终止方式以及 pthread_join 使用
运行结果
注意事项
5、实例场景分析
6、为什么线程退出时只能获取线程退出码?
7、总结
二、线程终止
1、从线程函数返回
示例代码
2、使用 pthread_exit 终止线程
3、使用 pthread_cancel 终止其他线程
为什么 pthread_cancel 没有立即生效?
1. 取消类型默认是延迟取消(Deferred Cancellation)
2. 取消点(Cancellation Points)
3. 在代码中
实际执行流程
为什么所有线程都能完成5次循环?
最终发生了什么?
总结
第一阶段:程序启动
第二阶段:所有线程运行中
第三阶段:主线程变成僵尸
第四阶段:子线程继续运行
第五阶段:所有线程结束
完整的时间线重建
重要发现
4、注意事项
1. pthread_exit 和 return 的返回值
2. pthread_cancel 的取消点
3. 资源释放
4. 主线程的终止(是终止阶段,不是取消阶段)
5、总结
三、线程终止与资源回收
1、线程终止的三种方式
(1)入口函数返回(正常终止)
(2)调用pthread_exit()(显式终止)
(3)线程被取消(异常终止)
2、线程终止的异常场景与进程影响
(1)线程异常终止的连锁反应
(2)进程终止的触发条件
3、线程资源回收:joinable vs detached
(1)joinable状态(默认)
(2)detached状态(先了解,后面会讲)
4、实践建议
(1)选择合适的终止方式
(2)合理管理线程状态
(3)异常处理策略
5、代码示例:分离线程与资源回收(了解)
6、总结
四、为什么 pthread_join 的第二个参数是 void** 类型?
1、函数传参的基本机制
2、pthread_join 的需求:获取线程返回值
为什么不能是 void*?
3、为什么是 void**?
(1)传递指针的地址
(2)pthread_join 的工作原理
4、类比回调函数:间接传递数据
5、总结:为什么是 void**?
6、关键结论
一、线程等待
在多线程编程中,线程等待(pthread_join)是一个重要的机制,用于同步线程的执行顺序,确保线程资源的正确回收,并获取线程的终止状态。以下是关于线程等待的详细说明。
1、为什么需要线程等待?
1. 资源回收
-
线程终止后,其占用的部分资源(如栈内存)不会自动释放,仍然存在于进程的地址空间中。
-
如果不调用
pthread_join,这些资源会一直占用,可能导致内存泄漏。
2. 避免僵尸线程
-
类似于进程中的僵尸进程,未被
join的终止线程会保留部分状态信息,影响系统资源管理。
3. 同步线程执行
-
主线程或其他线程可能需要等待某个线程完成特定任务后才能继续执行。
4. 获取线程返回值
-
通过
pthread_join可以获取线程的退出状态或返回值,用于后续逻辑处理。
2、pthread_join 函数
功能:阻塞调用线程,直到目标线程终止,并回收目标线程的资源。
原型:
int pthread_join(pthread_t thread, void **value_ptr);

参数:
-
thread:目标线程的 ID。 -
value_ptr:指向指针的指针,用于存储目标线程的返回值。-
如果不需要返回值,可以传递
NULL。 -
如果目标线程被取消(
pthread_cancel),*value_ptr会被设置为PTHREAD_CANCELED。
-
返回值:
-
成功时返回
0。 -
失败时返回非零错误码(如
ESRCH(线程不存在)、EINVAL(线程不可 join)或EDEADLK(死锁))。
3、线程终止状态与 pthread_join
该函数会阻塞当前线程,直至指定ID的thread线程结束运行。线程的终止方式不同,通过pthread_join获取的终止状态也会有所差异。目标线程的终止方式会影响 pthread_join 获取的返回值:
-
通过
return返回:value_ptr指向线程函数的返回值。 -
通过
pthread_exit退出:value_ptr指向pthread_exit的参数。 -
被
pthread_cancel取消:value_ptr被设置为PTHREAD_CANCELED。 -
不关心返回值:传递
NULL给value_ptr。
通过grep命令检索可发现,PTHREAD_CANCELED实际上是定义在<pthread.h>头文件中的一个宏,其实质值就是-1。
grep -ER "PTHREAD_CANCELED" /usr/include/
-
-E:使用扩展正则表达式 -
-R:递归搜索(遍历所有子目录)

从输出可以看到:
-
PTHREAD_CANCELED是一个宏定义 -
值为
((void *) -1)- 将 -1 强制转换为 void 指针 -
定义位置:
-
/usr/include/pthread.h- 主线程头文件 -
/usr/include/bits/pthread.h- 架构相关的线程头文件
-
4、演示三种线程终止方式以及 pthread_join 使用
以下代码归纳演示了三种线程终止方式(return、pthread_exit、pthread_cancel)以及 pthread_join 的使用:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>// 线程1:通过 return 返回
void *thread1(void *arg) {printf("Thread 1 returning...\n");int *p = malloc(sizeof(int));*p = 1;return (void *)p;
}// 线程2:通过 pthread_exit 退出
void *thread2(void *arg) {printf("Thread 2 exiting...\n");int *p = malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}// 线程3:无限循环,需被取消
void *thread3(void *arg) {while (1) {printf("Thread 3 is running...\n");sleep(1);}return NULL;
}int main() {pthread_t tid;void *ret;// 等待线程1(return)pthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("Thread 1 returned, ID: %lx, code: %d\n", tid, *(int *)ret);free(ret);// 等待线程2(pthread_exit)pthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("Thread 2 returned, ID: %lx, code: %d\n", tid, *(int *)ret);free(ret);// 等待线程3(pthread_cancel)pthread_create(&tid, NULL, thread3, NULL);sleep(3); // 主线程等待3秒pthread_cancel(tid); // 取消线程3pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED) {printf("Thread 3 canceled, ID: %lx\n", tid);} else {printf("Thread 3 unexpected return, ID: %lx\n", tid);}return 0;
}
运行结果


注意事项
-
线程 ID 复用:即使线程终止,其 ID 也不会被立即复用。
pthread_join可以确保资源释放后,ID 才可能被重用。 -
死锁风险:如果线程 A 尝试
join线程 B,而线程 B 也在join线程 A,会导致死锁。 -
分离线程:如果线程被设置为“分离状态”(
pthread_detach),则不能再对其调用pthread_join,否则会返回EINVAL。 -
返回值内存管理:线程返回的指针(如
malloc分配的内存)需由调用pthread_join的线程手动释放。
5、实例场景分析
例如,以下代码忽略线程的退出信息,将pthread_join函数的第二个参数设为NULL。线程结束后,程序会打印该线程的编号及其ID。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}return NULL;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer); // 使用完后释放内存}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){pthread_join(tid[i], NULL);printf("thread %d[%lu]...quit\n", i, tid[i]);}return 0;
}
执行代码后可以观察到,主线程创建的五个新线程各自完成一次打印操作后便正常终止,同时主线程成功实现了对这五个子线程的同步等待。

接下来我们探讨如何获取线程退出时的退出码。为了更直观地展示效果,我们预先将线程退出码设置为一个特定值,待线程成功结束时输出该退出码。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}return (void*)2025;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer); // 使用完后释放内存}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);//printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret); }return 0;
}
-
ret是void*类型(指针,通常是 64 位系统上的 8 字节) -
若是(int)ret,则是试图将指针强制转换为int类型(通常是 4 字节) -
在 64 位系统上,指针是 8 字节,int 是 4 字节,可能丢失数据,推荐使用
long类型
执行代码后,即可获取各线程的退出状态码信息。

注意: pthread_join函数默认是以阻塞的方式进行线程等待的。
6、为什么线程退出时只能获取线程退出码?
当等待进程退出时,我们可以通过wait或waitpid函数的status参数获取进程的退出码、终止信号以及core dump标志。但等待线程时为何只能获取退出码?线程难道不会出现异常吗?
实际上线程和进程一样,都可能以三种方式终止:
-
正常执行完成并返回正确结果
-
正常执行完成但返回错误结果
-
异常终止
虽然线程也存在异常终止的情况,但pthread_join函数无法获取这类信息。这是因为线程是进程内部的执行单元。如果某个线程崩溃,整个进程都会随之终止,当然也是包括main线程和其他创建的线程。这种情况下,我们根本没有机会调用pthread_join,因为进程已经退出!!!线程的join操作是基于线程正常执行完成的场景,无需处理异常信号——异常信号的处理属于进程层面的职责范畴!!!
举例来说,若在线程函数中制造除零错误,当线程执行到该处时会导致整个进程崩溃,从而无法执行后续的join操作。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;int a = 1 / 0; //error}return (void*)2022;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer); // 使用完后释放内存}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);}return 0;
}
执行代码时会发现,当任意线程崩溃时,整个进程也会随之终止。此时主线程甚至来不及等待新线程完成,这反映出多线程机制的脆弱性——只要进程中的单个线程异常退出,就会导致整个进程崩溃。更棘手的是,我们只能检测到进程崩溃,却无法精确定位是哪个具体线程引发了问题。

因此,pthread_join函数仅能在线程正常终止时获取其退出状态码,这种机制主要用于验证线程执行结果的正确性。
7、总结
-
pthread_join的作用:同步线程、回收资源、获取返回值。 -
终止方式的影响:不同终止方式会导致
pthread_join获取不同的状态。 -
资源管理:必须调用
pthread_join或pthread_detach避免内存泄漏。 -
错误处理:检查返回值以确保操作成功。
通过合理使用 pthread_join,可以确保多线程程序的稳定性和资源正确性。
二、线程终止
在多线程编程中,线程的终止是一个重要的环节。与进程终止不同,线程的终止需要更加精细的控制,以确保资源的正确释放和程序的稳定性。
若要仅终止某个线程而不影响整个进程,可采用以下三种方法:
-
让线程函数直接返回(主线程除外,因为主线程返回等同于调用exit)
-
线程调用pthread_exit自行终止
-
通过pthread_cancel终止同一进程中的其他线程
1、从线程函数返回
-
线程函数可以通过
return语句来终止自身。 -
这种方法适用于大多数线程,但对于主线程(即
main函数所在的线程)并不适用。 -
在
main函数中调用return相当于调用exit,会导致整个进程的终止,此时该进程已申请的资源将被释放,其余线程因资源不足也会随之终止。
示例代码
下面的代码中,我们尝试在 main 函数中调用 return:主线程创建五个新线程后,并没有进行线程等待(pthread_join() 函数),而是立即返回,此时整个进程就会终止运行。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){sleep(1);printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());}return (void*)0;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer); // 使用完后释放内存}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());return 0;
}
运行代码时,由于主线程提前终止导致整个进程退出,因此无法观察到新线程执行的打印操作:

下面的代码中,我们尝试在线程函数通过 return 语句来终止自身,而main函数的这个线程进行pthread_join(tid, NULL); 等待线程结束:
#include <pthread.h>
#include <stdio.h>void *thread_func(void *arg) {printf("Thread is running\n");return NULL; // 线程终止
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, NULL); // 等待线程结束printf("Main thread continues\n");return 0;
}
运行代码时,由于主线程等待线程退出和新线程的return退出操作,因此观察到新线程执行的打印操作:

2、使用 pthread_exit 终止线程
pthread_exit 函数用于显式地终止当前线程。与 return 不同,pthread_exit 可以在线程的任何位置调用,并且可以传递一个返回值给其他线程。
这个函数也与exit函数不同!!!exit函数的作用是终止进程,任何一个线程调用exit函数也代表的是整个进程终止!!!
函数原型:
void pthread_exit(void *value_ptr);

参数value_ptr:
-
指向线程终止时返回值的指针。一般指线程退出时的退出码信息。
-
需要注意的是,
value_ptr不应指向线程的局部变量,因为局部变量在线程终止后会被销毁。 -
通常,
value_ptr指向全局变量或动态分配的内存(如malloc分配的内存)。
需要说明的是:该函数没有返回值。与进程类似,线程结束时无法向其自身返回任何值。
无论是通过pthread_exit还是return返回的指针,其指向的内存必须是以下两种类型之一:
-
全局变量所在的内存区域
-
通过malloc分配的内存空间
禁止使用线程函数栈上的内存分配,因为当其他线程获取到这个返回指针时,线程函数已经退出,其栈内存将被回收。
示例代码:
例如,以下代码通过调用pthread_exit函数来终止线程,并将线程的退出状态值设为2025。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}pthread_exit((void*)2025);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer); // 使用完后释放内存}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);}return 0;
}
执行代码后,我们可以观察到线程退出时会返回预设的退出码2025。

以下代码通过调用pthread_exit函数来终止线程,并将线程的退出返回值设为int* 类型,也就是在新线程里面malloc出来的result指针,作为参数传入pthread_exit() 函数:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>void *thread_func(void *arg) {int *result = malloc(sizeof(int));*result = 42;pthread_exit(result); // 线程终止并返回结果
}int main() {pthread_t tid;void *retval;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, &retval); // 等待线程结束并获取返回值printf("Thread returned: %d\n", *(int *)retval);free(retval); // 释放动态分配的内存return 0;
}

3、使用 pthread_cancel 终止其他线程
pthread_cancel 函数用于取消同一个进程中的另一个线程的执行。被取消的线程会在下一个取消点(如 pthread_testcancel 或某些阻塞系统调用)处终止。
函数原型:
int pthread_cancel(pthread_t thread);

参数thread:要取消的线程的 ID(用户级)。
返回值:
-
成功时返回
0。 -
失败时返回非零错误码。
示例代码:
线程支持自我取消功能,成功取消的线程通常返回-1作为退出码。以下代码示例展示了线程完成打印操作后自动取消自身的过程。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;pthread_cancel(pthread_self());}pthread_exit((void*)2025);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);}return 0;
}
执行代码后可以观察到,每个线程在完成一次打印操作后即终止,但其退出码显示为-1而非我们预设的2025。这是因为系统在线程调用pthread_exit函数之前就触发了线程取消操作。

线程虽然可以自行终止,但通常不采用这种操作方式。更常见的做法是由一个线程终止另一个线程,比如主线程终止其创建的子线程。
以下示例展示了这种用法:代码在创建五个线程后,立即终止了编号为0至3的四个子线程。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;}pthread_exit((void*)2025);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);free(buffer);}pthread_cancel(tid[0]);pthread_cancel(tid[1]);pthread_cancel(tid[2]);pthread_cancel(tid[3]);printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);}return 0;
}
观察运行结果可以看到,0-3号线程的退出码并非我们设置的2025,只有未被取消的4号线程正确返回了预设值2025。这是因为4号线程是唯一未被取消的执行线程。

此外,新线程同样具备取消主线程的能力。例如,在下述示例中,我们让所有线程都尝试取消主线程。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>pthread_t main_thread;void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;pthread_cancel(main_thread);}free(msg);pthread_exit((void*)2025);
}
int main()
{main_thread = pthread_self();pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tid[i], (long)ret);}return 0;
}
执行代码时,配合以下监控脚本实现实时监测:
while :; do ps -aL | head -1&&ps -aL | grep thread_ids | grep -v grep;echo "###############";sleep 1;done
一段时间后可以观察到,六个线程中PID和LWP相同的线程(即主线程)右侧显示为<defunct>状态。这表明主线程已被终止,因此我们无法继续观察到主线程等待新线程时打印的退出码信息。

输出结果:
thread 0 tid is 140712403252992
I am thread 0...pid: 834882, ppid: 801934, tid: 140712403252992
thread 1 tid is 140712394860288
thread 2 tid is 140712386467584
I am thread 2...pid: 834882, ppid: 801934, tid: 140712386467584
I am thread 1...pid: 834882, ppid: 801934, tid: 140712394860288
thread 3 tid is 140712378074880
I am thread 3...pid: 834882, ppid: 801934, tid: 140712378074880
thread 4 tid is 140712369682176
I am main thread...pid: 834882, ppid: 801934, tid: 140712403257152
I am thread 4...pid: 834882, ppid: 801934, tid: 140712369682176
I am thread 0...pid: 834882, ppid: 801934, tid: 140712403252992
I am thread 2...pid: 834882, ppid: 801934, tid: 140712386467584
I am thread 1...pid: 834882, ppid: 801934, tid: 140712394860288
I am thread 3...pid: 834882, ppid: 801934, tid: 140712378074880
I am thread 4...pid: 834882, ppid: 801934, tid: 140712369682176
I am thread 1...pid: 834882, ppid: 801934, tid: 140712394860288
I am thread 0...pid: 834882, ppid: 801934, tid: 140712403252992
I am thread 3...pid: 834882, ppid: 801934, tid: 140712378074880
I am thread 2...pid: 834882, ppid: 801934, tid: 140712386467584
I am thread 4...pid: 834882, ppid: 801934, tid: 140712369682176
I am thread 1...pid: 834882, ppid: 801934, tid: 140712394860288
I am thread 3...pid: 834882, ppid: 801934, tid: 140712378074880
I am thread 0...pid: 834882, ppid: 801934, tid: 140712403252992
I am thread 2...pid: 834882, ppid: 801934, tid: 140712386467584
I am thread 4...pid: 834882, ppid: 801934, tid: 140712369682176
I am thread 1...pid: 834882, ppid: 801934, tid: 140712394860288
I am thread 3...pid: 834882, ppid: 801934, tid: 140712378074880
I am thread 0...pid: 834882, ppid: 801934, tid: 140712403252992
I am thread 2...pid: 834882, ppid: 801934, tid: 140712386467584
I am thread 4...pid: 834882, ppid: 801934, tid: 140712369682176
上面的输出结果很有意思!这说明 pthread_cancel 没有立即生效,从输出可以看到:
-
✅ 所有5个线程都完成了完整的5次循环
-
✅ 主线程打印了信息但没有被立即取消
-
❌ 没有看到线程退出信息(说明主线程最终还是被取消了)
为什么 pthread_cancel 没有立即生效?
1. 取消类型默认是延迟取消(Deferred Cancellation)
-
默认的取消类型是 PTHREAD_CANCEL_DEFERRED
-
线程只会在取消点(cancellation points)检查取消请求
2. 取消点(Cancellation Points)
-
常见的取消点包括:
sleep()、printf()、read(),write()、pthread_join()等系统调用
3. 在代码中
while (count < 5){printf("I am %s...\n", msg); // 取消点sleep(1); // 取消点count++;pthread_cancel(main_thread); // 设置取消标志,但不立即生效
}
实际执行流程
子线程执行:
-
每次循环调用
printf和sleep(都是取消点) -
但这些取消点检查的是当前线程自己的取消状态,不是主线程的
主线程执行:
-
主线程进入
pthread_join(tid[0], &ret)等待 -
pthread_join是一个取消点 -
当主线程在
pthread_join中检查取消状态时,才发现自己被取消了
为什么所有线程都能完成5次循环?
-
子线程的
pthread_cancel(main_thread)只是设置主线程的取消标志 -
主线程继续执行直到遇到取消点(
pthread_join) -
在等待第一个线程完成时,其他4个线程继续运行完成所有循环
最终发生了什么?
在输出结束后:
-
所有子线程完成5次循环,调用
free(msg)和pthread_exit -
主线程在
pthread_join(tid[0], &ret)中检查取消状态 -
发现取消标志被设置,立即终止
-
进程结束,不会打印任何线程退出信息
总结
输出证明了:
-
pthread_cancel是异步的,不立即生效 -
线程取消只在取消点检查
-
主线程在
pthread_join中才响应取消请求 -
所有子线程在此之前已经完成了工作
这是一个很好的多线程取消机制的实际演示!!!
观察结果:
PID LWP TTY TIME CMD834882 834882 pts/1 00:00:00 thread_ids834882 834883 pts/1 00:00:00 thread_ids834882 834884 pts/1 00:00:00 thread_ids834882 834885 pts/1 00:00:00 thread_ids834882 834886 pts/1 00:00:00 thread_ids834882 834887 pts/1 00:00:00 thread_ids
###############PID LWP TTY TIME CMD834882 834882 pts/1 00:00:00 thread_ids <defunct>834882 834883 pts/1 00:00:00 thread_ids834882 834884 pts/1 00:00:00 thread_ids834882 834885 pts/1 00:00:00 thread_ids834882 834886 pts/1 00:00:00 thread_ids834882 834887 pts/1 00:00:00 thread_ids
###############PID LWP TTY TIME CMD834882 834882 pts/1 00:00:00 thread_ids <defunct>834882 834883 pts/1 00:00:00 thread_ids834882 834884 pts/1 00:00:00 thread_ids834882 834885 pts/1 00:00:00 thread_ids834882 834886 pts/1 00:00:00 thread_ids834882 834887 pts/1 00:00:00 thread_ids
###############PID LWP TTY TIME CMD834882 834882 pts/1 00:00:00 thread_ids <defunct>834882 834883 pts/1 00:00:00 thread_ids834882 834884 pts/1 00:00:00 thread_ids834882 834885 pts/1 00:00:00 thread_ids834882 834886 pts/1 00:00:00 thread_ids834882 834887 pts/1 00:00:00 thread_ids
###############PID LWP TTY TIME CMD834882 834882 pts/1 00:00:00 thread_ids <defunct>834882 834883 pts/1 00:00:00 thread_ids834882 834884 pts/1 00:00:00 thread_ids834882 834885 pts/1 00:00:00 thread_ids834882 834886 pts/1 00:00:00 thread_ids834882 834887 pts/1 00:00:00 thread_ids
###############PID LWP TTY TIME CMD
###############PID LWP TTY TIME CMD
###############PID LWP TTY TIME CMD
###############PID LWP TTY TIME CMD
###############PID LWP TTY TIME CMD
###############
第一阶段:程序启动
PID LWP TTY TIME CMD
###############
-
程序刚开始,进程还未完全启动
第二阶段:所有线程运行中
PID LWP TTY TIME CMD834499 834499 pts/1 00:00:00 thread_ids # 主线程834499 834500 pts/1 00:00:00 thread_ids # 子线程0834499 834501 pts/1 00:00:00 thread_ids # 子线程1 834499 834502 pts/1 00:00:00 thread_ids # 子线程2834499 834503 pts/1 00:00:00 thread_ids # 子线程3834499 834504 pts/1 00:00:00 thread_ids # 子线程4
-
6个线程都在运行:1个主线程 + 5个子线程
-
对应代码输出中所有线程都在循环打印信息
第三阶段:主线程变成僵尸
PID LWP TTY TIME CMD834499 834499 pts/1 00:00:00 thread_ids <defunct> # 主线程变僵尸834499 834500 pts/1 00:00:00 thread_ids # 子线程0834499 834501 pts/1 00:00:00 thread_ids # 子线程1834499 834502 pts/1 00:00:00 thread_ids # 子线程2834499 834503 pts/1 00:00:00 thread_ids # 子线程3834499 834504 pts/1 00:00:00 thread_ids # 子线程4
关键转折点:
-
主线程(LWP 834499)变成
<defunct>(僵尸) -
但5个子线程还在运行
-
这说明:主线程被取消了,但进程没有立即结束
第四阶段:子线程继续运行
重复多次:
PID LWP TTY TIME CMD834499 834499 pts/1 00:00:00 thread_ids <defunct>834499 834500 pts/1 00:00:00 thread_ids834499 834501 pts/1 00:00:00 thread_ids834499 834502 pts/1 00:00:00 thread_ids834499 834503 pts/1 00:00:00 thread_ids834499 834504 pts/1 00:00:00 thread_ids
-
主线程保持僵尸状态
-
5个子线程继续执行完成它们的5次循环
第五阶段:所有线程结束
PID LWP TTY TIME CMD
###############
-
所有线程都结束了,进程完全退出
完整的时间线重建
-
0-1秒:程序启动,所有线程开始运行
-
约1-2秒:某个子线程的
pthread_cancel(main_thread)生效 -
关键时刻:主线程在
pthread_join中检查到取消请求,变成僵尸 -
但进程没有结束:因为还有5个子线程在运行
-
子线程继续:完成剩余的循环次数(你的输出显示完成了5次)
-
所有子线程退出:进程最终结束
重要发现
这个输出揭示了几个关键点:
-
pthread_cancel不立即终止进程:只是让目标线程变成僵尸 -
进程存活条件:只要还有非僵尸线程,进程就继续运行
-
主线程的特殊性:即使主线程变成僵尸,其他线程仍可继续执行
-
最终清理:当所有线程都结束时,进程才完全退出
代码输出显示所有线程都完成了5次循环,这说明:
-
主线程被取消后,子线程确实继续运行完成了工作
-
进程没有立即终止,而是等待所有子线程自然结束
注意事项:
-
采用这种取消方式时,主线程与子线程处于平等地位。取消任一线程后,其他线程仍可继续执行,但主线程将终止后续代码的运行。
-
通常情况下,我们更推荐使用主线程来控制子线程,这更符合常规的线程控制逻辑。虽然实验证明子线程可以取消主线程,但这种做法并不推荐。
4、注意事项
1. pthread_exit 和 return 的返回值
-
使用
pthread_exit或return返回的指针必须指向全局变量或动态分配的内存(如malloc分配的内存),而不能指向线程函数的局部变量。 -
因为局部变量在线程函数退出后会被销毁,其他线程访问时会导致未定义行为。
2. pthread_cancel 的取消点
-
被取消的线程不会立即终止,而是会在下一个取消点(如
pthread_testcancel、read、write等系统调用)处终止。 -
可以通过
pthread_setcancelstate和pthread_setcanceltype控制取消行为。
3. 资源释放
-
线程终止时,操作系统会自动释放线程占用的资源(如栈内存),但动态分配的内存(如
malloc分配的内存)需要手动释放,否则会导致内存泄漏。
4. 主线程的终止(是终止阶段,不是取消阶段)
-
主线程(
main函数)终止时,整个进程会退出,所有其他线程也会被强制终止。 -
因此,如果需要等待其他线程完成后再终止主线程,应使用
pthread_join。
5、总结
线程终止是多线程编程中的关键操作,需要根据具体场景选择合适的方法:
-
使用
return或pthread_exit终止当前线程。 -
使用
pthread_cancel终止其他线程(需谨慎使用,避免资源泄漏)。 -
确保正确处理线程的返回值和资源释放,避免内存泄漏和未定义行为。
三、线程终止与资源回收
1、线程终止的三种方式
线程作为进程内的执行单元,其终止方式直接影响资源回收和进程稳定性。以下是线程终止的核心机制:
(1)入口函数返回(正常终止)
机制:线程的入口函数(如void* Routine(void* arg))执行return语句时,线程正常终止,返回值可通过pthread_join获取。
特点:线程的退出状态由返回值决定,可传递任意类型数据(需强制转换为void*)。
示例:
void* thread_func(void* arg) {int* result = malloc(sizeof(int));*result = 42;return result; // 线程终止并返回动态分配的内存地址
}
(2)调用pthread_exit()(显式终止)
机制:线程主动调用void pthread_exit(void *retval)终止自身,retval为退出状态(可通过pthread_join获取)。
注意事项:切勿使用exit():exit()会终止整个进程,导致所有线程异常退出。
示例:
void* thread_func(void* arg) {pthread_exit((void*)42); // 线程显式退出,返回值为42
}
(3)线程被取消(异常终止)
机制:其他线程通过pthread_cancel()请求终止目标线程,目标线程的退出状态为PTHREAD_CANCELED(值为-1的宏定义)。
特点:线程需设置取消状态(pthread_setcancelstate())和类型(pthread_setcanceltype())以响应取消请求。
示例:
void* thread_func(void* arg) {// 允许线程被取消pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);// 延迟以模拟长时间任务,期间可能被取消for (int i = 0; i < 10; i++) {sleep(1);}return NULL;
}
// 主线程中取消子线程
pthread_cancel(tid);
2、线程终止的异常场景与进程影响
(1)线程异常终止的连锁反应
未捕获的异常:若线程因未处理信号(如SIGSEGV)崩溃,默认行为会终止整个进程(包括所有线程)。
pthread_join的局限性:
-
pthread_join仅适用于线程正常终止的场景,无法捕获异常终止信息。 -
进程一旦崩溃,所有线程资源由操作系统自动回收,无需手动干预。
(2)进程终止的触发条件
主线程退出:主线程(main函数)结束时,若进程未设置分离线程或注册清理函数,进程将直接终止。
关键原则:
-
进程是资源分配的基本单位,线程崩溃或主线程退出均会导致进程终止。
-
异常信号(如
SIGABRT)的处理属于进程级职责,需通过信号处理器(signal()或sigaction())统一管理。
3、线程资源回收:joinable vs detached
(1)joinable状态(默认)
特性:
-
线程终止后,其资源(如栈内存、线程描述符)不会立即释放,需由其他线程调用
pthread_join回收。 -
若未调用
pthread_join且线程未分离,会导致资源泄漏(僵尸线程)。
适用场景:需获取线程返回值或确保线程执行完毕后再继续主线程逻辑。
(2)detached状态(先了解,后面会讲)
设置方式:
-
创建线程时通过属性参数设置:
pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&tid, &attr, thread_func, NULL); -
动态分离已创建的线程:
pthread_detach(tid);
特性:
-
线程终止后自动释放资源,无需
pthread_join。 -
无法通过
pthread_join获取返回值,适合无需关心线程结果的场景(如后台任务)。
关键点:
-
分离线程仍属于进程地址空间,可访问进程资源。
-
主线程无需等待分离线程,提高程序响应性。
4、实践建议
(1)选择合适的终止方式
-
正常终止:优先使用
return或pthread_exit(),确保资源清理和返回值传递。 -
避免
exit():exit()会强制终止进程,导致资源未释放和潜在数据损坏。
(2)合理管理线程状态
-
默认
joinable:若需获取线程结果或确保执行顺序,使用pthread_join。 -
分离线程:对无需同步的后台线程,及时设置为
detached,避免资源泄漏。
(3)异常处理策略
-
信号处理:在进程级注册信号处理器,统一处理线程崩溃等异常。
-
日志记录:记录线程异常信息,便于问题排查(如通过
stderr或日志文件)。
5、代码示例:分离线程与资源回收(了解)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* background_task(void* arg) {printf("Background thread working...\n");sleep(2);printf("Background thread done.\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, background_task, NULL);pthread_attr_destroy(&attr);printf("Main thread continues without waiting.\n");sleep(3); // 模拟主线程其他任务return 0; // 进程退出时,分离线程资源自动回收
}
6、总结
-
线程终止方式:优先使用
return或pthread_exit(),避免exit()。 -
异常处理:线程崩溃会终止进程,需通过进程级信号处理统一管理。
-
资源回收:根据需求选择
joinable(需pthread_join)或detached(自动回收)。 -
最佳实践:分离后台线程,主线程专注核心逻辑,提升程序健壮性和效率。
四、为什么 pthread_join 的第二个参数是 void** 类型?
pthread_join 是 POSIX 线程库中用于等待线程终止并回收其资源的核心函数。其函数原型如下:
int pthread_join(pthread_t thread, void **retval);
第二个参数 void **retval 的设计看似复杂,但通过类比 C 语言的传地址、拷贝和回调函数机制,可以清晰地理解其必要性。

1、函数传参的基本机制
在 C 语言中,函数参数传递主要有两种方式:
-
值传递:函数接收参数的副本,修改副本不会影响原数据。
-
地址传递(传指针):函数接收数据的地址,可直接修改原数据。
类比示例
#include <stdio.h>// 值传递:无法修改原变量
void modify_value(int x) {x = 42; // 仅修改副本
}// 地址传递:可修改原变量
void modify_pointer(int *x) {*x = 42; // 通过指针修改原数据
}int main() {int a = 0;modify_value(a);printf("a after modify_value: %d\n", a); // 输出 0modify_pointer(&a);printf("a after modify_pointer: %d\n", a); // 输出 42return 0;
}
关键点:若希望函数修改外部数据,必须传递该数据的地址(指针)。
2、pthread_join 的需求:获取线程返回值
线程返回值:线程的入口函数(如 void* thread_func(void* arg))可以通过 return 或 pthread_exit() 返回一个 void* 类型的值。
主线程需求:主线程需要获取这个返回值,但:
-
返回值可能是动态分配的内存、结构体指针或任意类型数据。
-
若直接传递值(
void*而非void**),pthread_join只能返回副本,无法传递动态数据的所有权。
为什么不能是 void*?
若 pthread_join 的第二个参数是 void*,函数内部只能修改该指针的副本,无法让主线程获取实际的返回值地址。
示例(错误设计):
// 假设的错误设计(实际不存在)
void pthread_join_wrong(pthread_t thread, void* retval) {// 无法修改主线程的 retval,因为传递的是副本
}
3、为什么是 void**?
(1)传递指针的地址
void** 是一个指向指针的指针,允许 pthread_join 修改主线程中的指针变量。
类比动态内存分配:
#include <stdlib.h>
#include <stdio.h>void allocate_memory(int **ptr) {*ptr = malloc(sizeof(int)); // 修改主线程的指针变量**ptr = 42; // 通过双重指针修改数据
}int main() {int *p = NULL;allocate_memory(&p); // 传递指针的地址printf("Value: %d\n", *p); // 输出 42free(p);return 0;
}
allocate_memory 通过 int** 修改主线程的 int* 变量 p,使其指向动态分配的内存。
(2)pthread_join 的工作原理
线程终止时:线程的返回值存储在某个内部位置(如线程控制块)。
pthread_join 的作用:
-
等待线程终止。
-
将线程的返回值地址写入
void**参数指向的变量。
示例:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>void* thread_func(void* arg) {int* value = malloc(sizeof(int));*value = 42;return value; // 返回动态分配的内存地址
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);void* retval = NULL; // 主线程的指针变量pthread_join(tid, &retval); // 传递指针的地址int* result = (int*)retval;printf("Thread returned: %d\n", *result); // 输出 42free(result); // 释放动态内存return 0;
}
pthread_join 通过 &retval(void** 类型)修改主线程的 retval,使其指向线程返回的动态内存。
4、类比回调函数:间接传递数据
回调函数中,常通过传递指针来允许函数修改外部数据。pthread_join 的设计类似:
回调函数示例:
#include <stdio.h>typedef void (*Callback)(int*);void worker(Callback cb) {int data = 42;cb(&data); // 传递数据的地址给回调
}void callback_func(int* value) {printf("Received value: %d\n", *value); // 修改或使用外部数据
}int main() {worker(callback_func);return 0;
}
pthread_join 的类比:线程终止时,pthread_join 充当“回调”,将线程的返回值地址写入主线程提供的 void** 变量。
5、总结:为什么是 void**?
-
修改主线程的指针变量:
void**允许pthread_join修改主线程中的指针变量,使其指向线程的返回值。 -
支持任意类型返回值:
void*可以指向任何类型的数据(需强制转换),void**则是其地址的通用表示。 -
内存管理责任转移:主线程通过
void**获取动态分配的返回值地址,并负责后续释放(如调用free)。 -
与 C 语言机制一致:类似动态内存分配或回调函数,通过传递指针的地址实现间接修改。
6、关键结论
-
pthread_join的第二个参数设计为void**:是为了让主线程能够获取线程返回值的地址,而非值的副本。 -
类比核心:
-
动态内存分配:通过
int**修改主线程的指针。 -
回调函数:通过指针传递实现间接数据访问。
-
-
实践建议:
-
使用
pthread_join后,检查返回值是否为PTHREAD_CANCELED(线程被取消)。 -
若线程返回动态内存,主线程需负责释放,避免内存泄漏。
-
