线程取消与清理:pthread_cancel的使用、取消点与异步取消、线程清理处理程序

线程取消这个话题,说实话,很多初学者容易踩坑。我早年在一个网络代理项目里,就因为对取消机制理解不到位,导致程序在高峰期莫名其妙地崩溃。从那以后,我对线程取消的每个细节都格外小心。

今天咱们就把线程取消这件事彻底讲透。你想想看,一个线程正在执行任务,突然另一个线程说“你别干了,停下来”,这中间会发生什么?资源怎么释放?锁怎么办?嗯,这就是我们要解决的核心问题。

一、pthread_cancel:发起取消请求

pthread_cancel 的作用是向指定线程发送取消请求。注意我的用词——是“请求”,不是“命令”。线程收到请求后,什么时候响应,取决于它的取消状态和取消类型。

#include <pthread.h>

int pthread_cancel(pthread_t thread);
// 成功返回0,失败返回错误码

函数很简单,传入线程ID就行。但这里有个坑:pthread_cancel 是异步的,它不会等待目标线程真正退出就返回了。如果你需要确认线程已经终止,得配合 pthread_join 使用。

注意: 线程取消不是强制终止。被取消的线程会继续执行,直到遇到下一个取消点。这一点和 pthread_kill 发送信号完全不同。

二、取消点与异步取消

什么是取消点?说白了,就是线程检查自己是否被取消的那些“检查站”。POSIX 标准定义了一系列函数作为取消点,比如:

  • pthread_joinpthread_cond_waitpthread_cond_timedwait
  • readwriteopenclose 等 I/O 函数
  • sleepusleepnanosleep
  • acceptsendrecv 等网络函数

线程只有在这些取消点处,才会响应取消请求。如果线程一直在做纯计算,没有调用任何取消点函数,那它永远不会被取消。

这就引出了两种取消类型:

取消类型 行为 适用场景
延迟取消(默认) 遇到取消点才响应 大多数情况,安全可靠
异步取消 随时可能被取消 极少使用,风险极高
核心原则: 我个人的习惯是,永远使用默认的延迟取消。异步取消就像在代码里埋了一颗定时炸弹,你不知道线程执行到哪一行就被干掉了,资源释放根本没法保证。

设置取消类型的函数:

int pthread_setcanceltype(int type, int *oldtype);
// type: PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS

我曾经在一个实时数据处理系统里,看到同事用了异步取消。结果线程在持有互斥锁的中间状态被取消,锁永远没释放,其他线程全部死等。排查了整整一个下午才找到原因。嗯,从那以后我对异步取消敬而远之。

三、线程清理处理程序

线程被取消时,如果已经分配了资源(比如 malloc 的内存、打开的文件、持有的锁),这些资源谁来释放?这就是 pthread_cleanup_pushpthread_cleanup_pop 的用武之地。

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

这两个函数必须成对出现,而且要在同一个作用域内。清理函数 routine 会在以下三种情况下被调用:

  1. 线程被取消(在取消点处)
  2. 线程主动调用 pthread_exit
  3. 调用 pthread_cleanup_pop(1) 且参数为非零

看个完整的例子:

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

void cleanup_handler(void *arg) {
    printf("清理处理程序执行,释放资源: %s\n", (char *)arg);
    free(arg);  // 释放动态分配的内存
}

void *thread_func(void *arg) {
    char *buffer = malloc(1024);
    if (!buffer) {
        pthread_exit(NULL);
    }
    
    // 注册清理处理程序
    pthread_cleanup_push(cleanup_handler, buffer);
    
    // 模拟工作,这里是一个取消点
    sleep(5);
    
    // 正常退出时,执行清理(参数1表示执行清理函数)
    pthread_cleanup_pop(1);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    
    sleep(1);  // 确保线程已进入sleep
    pthread_cancel(tid);  // 发送取消请求
    
    pthread_join(tid, NULL);
    printf("主线程结束\n");
    return 0;
}
小技巧: 我习惯把 pthread_cleanup_push/pop 看作是一个“资源保护罩”。在获取资源后立即 push,在释放资源前 pop。这样即使线程被取消,资源也不会泄漏。

四、禁用取消与恢复

有时候,某些关键操作不能被中断。比如正在更新一个链表,或者正在写入文件。这时候可以暂时禁用取消:

int pthread_setcancelstate(int state, int *oldstate);
// state: PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE

用法很简单:

int oldstate;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
// 执行关键操作,不会被取消
pthread_setcancelstate(oldstate, NULL);  // 恢复原状态
重要提醒: 禁用取消时,一定要记得恢复。我曾经见过一个 bug,线程在某个分支里禁用了取消但忘记恢复,导致这个线程永远无法被取消,最终造成资源泄漏。建议用 if-else 或 goto 确保恢复逻辑一定会执行。

五、知识体系总览

下面这张图,把线程取消的整个流程串起来了。我建议你仔细看看,理解每个环节之间的关系。

线程取消知识体系 pthread_cancel() 取消状态:ENABLE / DISABLE DISABLE:忽略取消请求 ENABLE:继续处理取消 取消类型:DEFERRED / ASYNCHRONOUS DEFERRED:在取消点响应 ASYNCHRONOUS:随时响应 清理处理程序:pthread_cleanup_push/pop 确保资源释放

从图中可以看到,线程取消的完整路径是:发起请求 → 检查状态 → 判断类型 → 到达取消点 → 执行清理。每一步都有对应的控制函数,环环相扣。

六、实战经验总结

最后,分享几个我在项目中积累的经验:

  • 永远优先使用延迟取消。异步取消只在你完全清楚后果的情况下使用,而且我建议你写清楚注释说明为什么必须用异步取消。
  • 清理处理程序要轻量。不要在清理函数里做复杂操作,比如再次加锁、分配内存。清理函数本身应该能快速、安全地执行完毕。
  • push/pop 要成对出现。编译器会检查这一点,但如果你用了 goto 或 longjmp,可能会跳过 pop,导致清理函数注册后没被移除。我见过一个案例,清理函数被重复执行,double free 导致程序崩溃。
  • 取消点函数要心里有数。写代码时,要清楚哪些函数是取消点。特别是在循环里调用取消点函数时,要考虑线程可能在任何一次迭代中被取消。
一句话总结: 线程取消不是暴力终止,而是一种协作机制。被取消的线程有权利、也有责任做好善后工作。用好 pthread_cleanup_push/pop,你的多线程代码才能既灵活又安全。

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