前段时间我们开启了关于线程部分的知识学习,我们也介绍了线程的创建,等待,取消,等内容。以及之后我们说了关于多线程的互斥问题。
昨天发现了一个非常有趣的东西叫做取消点,所以今天这篇文章我将会为大家补充一下取消点的相关知识。
重要性肯定没有之前那些知识大,但是了解一下其实也挺好的。
我们之前在线程的控制中讲过pthread_cancel这个函数:

我们说:它的作用类似于向目标线程发送一个“终止请求”,但具体是否终止、何时终止以及如何清理资源,取决于目标线程的取消状态和清理处理机制。
这里有提到一个关键的话:是否终止?何时终止?
诶,何时终止我倒能勉强理解,但是你这个是否终止是什么意思?难不成,这个终止还会取消吗?
是的,线程是否真正终止,取决于它的取消状态(cancellation state)和取消类型(cancellation type),这两者由以下函数控制:
pthread_setcancelstate(int state, int *oldstate); pthread_setcanceltype(int type, int *oldtype);
一个线程具有两种取消状态:
PTHREAD_CANCEL_ENABLE:默认状态,即在此状态下,线程才可以响应取消请求。
PTHREAD_CANCEL_DISABLE:线程会忽略所有取消请求,直到状态重新变为 ENABLE。
如果线程禁用取消,pthread_cancel 的请求会被暂时挂起,不会生效。
所以取消线程是完全有可能不生效的。
即便你的状态是默认的,响应了取消请求。
此时还有一个叫做取消类型的东西:
PTHREAD_CANCEL_DEFERRED(默认):延迟取消,线程只会在下一个取消点(如 sleep、read、write 等系统调用)时终止。
PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,线程可能在任何指令处被立即终止(十分危险,锁未释放(死锁风险),内存泄漏等风险,除非你十分清楚自己在干嘛,否则基本不使用)
这里我们就引出了一个概念:取消点?
所以我们线程究竟取不取消,就是看取消点呗?
那么什么是取消点呢?
取消点是 POSIX 线程(pthread)中定义的一些特殊函数或系统调用,在这些点上,如果线程收到取消请求(pthread_cancel),并且它的取消状态是 ENABLE、取消类型是 DEFERRED(默认),那么线程就会在这里被终止。
由此,取消点也可能会引出一系列的问题。比如,在副线程执行纯运算死循环时:
请看这个代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* compute_loop(void* arg)
{
while (1)
{ // 无取消点
int x = 0;
for (int i = 0; i < 1000000; i++) x += i;
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, compute_loop, NULL);
sleep(1);
pthread_cancel(tid); // 发送取消请求
printf("Cancel sent, waiting...\n");
pthread_join(tid, NULL); // 卡死在这里,线程不会退出
return 0;
}我们可以看见,在主线程中我们是调用了cancel函数的:

可是结果却一直阻塞住了。
造成这一切的原因就是在我们的副线程运行的代码中,没有出现任何取消点。
代码中找不到取消点,哪怕你最后收到了取消请求,我们也无法处理。
那哪些情况下没有取消点,哪些情况下有呢?
实际上,没有取消点的情况很少,基本上就只有我们刚刚测试的这一种情况: 纯CPU密集型计算
因为POSIX 明确规定以下操作必须是取消点:
类别 | 示例函数 |
|---|---|
文件I/O | read, write, fsync, open |
网络I/O | recv, send, accept |
进程同步 | sleep, nanosleep, pause |
线程同步 | pthread_cond_wait, pthread_join |
包括动态内存分配等调用,绝大都看作一个取消点。
void* print_loop(void* arg)
{
while (1)
{
printf("Running...\n"); // printf 可能是取消点
}
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, print_loop, NULL);
sleep(1);
pthread_cancel(tid); // 发送取消请求
printf("Cancel sent, waiting...\n");
pthread_join(tid, NULL); // 卡死在这里,线程不会退出
printf("退出成功\n");
return 0;
}当我们把代码换成这个之后,就能正常的取消了,由此可见在linux系统下,printf内部封装的write是一个取消点。
除此之外,我们还可以专门用显示的调用接口:pthread_testcancel()。
该函数调用放在副线程内部,可以显式的充当一个取消点,随后进行取消状态的检测。
除了前面的pthread_cancel的取消方法,我们还有一种取消线程的方法:协作式取消。
相比 pthread_cancel 的强制终止,协作式取消通过共享变量通知线程“优雅退出”,避免了资源泄漏和不确定性问题。
其核心思想就是定义一个全局标志(如 volatile bool should_exit),由主线程控制,工作线程定期检查该标志并主动退出。
这里我们提到了volatile这个关键字,其实这个关键字我们应该放在信号章节进行讲解。
这个关键字有什么作用呢?:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
什么意思呢?在volatile bool shoud_exit中,就是为了保证每次访问shoud_exit都直接从内存读取,禁止编译器优化。(在某些优化中,可能会直接读取存储在CPU中的老旧的值,而不是每次都去内存中读取。如果这个值出现的变化,你不从内存读取新值,读的就会一直是老的值,导致出现问题。)
如果要使用协作式取消,就是这样:
atomic_bool should_exit = false; // 原子退出标志
void* thread_func(void* arg)
{
while (!atomic_load(&should_exit))
{ // 原子读取
// 工作逻辑...
}
// 清理资源...
return NULL;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// ... 运行一段时间后 ...
atomic_store(&should_exit, true); // 原子写入
pthread_join(tid, NULL);
return 0;
}希望补充的知识点对大家有所帮助!!