首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >关于前文:取消点的补充

关于前文:取消点的补充

作者头像
海棠未眠
发布2025-10-22 16:04:01
发布2025-10-22 16:04:01
5300
举报
运行总次数:0

前言:

前段时间我们开启了关于线程部分的知识学习,我们也介绍了线程的创建,等待,取消,等内容。以及之后我们说了关于多线程的互斥问题。

昨天发现了一个非常有趣的东西叫做取消点,所以今天这篇文章我将会为大家补充一下取消点的相关知识。

重要性肯定没有之前那些知识大,但是了解一下其实也挺好的。

一、取消点

我们之前在线程的控制中讲过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(默认):延迟取消,线程只会在下一个取消点(如 sleepreadwrite 等系统调用)时终止。

PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,线程可能在任何指令处被立即终止(十分危险,锁未释放(死锁风险),内存泄漏等风险,除非你十分清楚自己在干嘛,否则基本不使用)

这里我们就引出了一个概念:取消点?

所以我们线程究竟取不取消,就是看取消点呗?

那么什么是取消点呢?


取消点是 POSIX 线程(pthread)中定义的一些特殊函数或系统调用,在这些点上,如果线程收到取消请求(pthread_cancel),并且它的取消状态是 ENABLE取消类型是 DEFERRED(默认),那么线程就会在这里被终止。 

由此,取消点也可能会引出一系列的问题。比如,在副线程执行纯运算死循环时:

请看这个代码:

代码语言:javascript
代码运行次数:0
运行
复制
#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

包括动态内存分配等调用,绝大都看作一个取消点。

代码语言:javascript
代码运行次数:0
运行
复制
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中的老旧的值,而不是每次都去内存中读取。如果这个值出现的变化,你不从内存读取新值,读的就会一直是老的值,导致出现问题。)

如果要使用协作式取消,就是这样:

代码语言:javascript
代码运行次数:0
运行
复制
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;
}

总结:

希望补充的知识点对大家有所帮助!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一、取消点
  • 二、协作式取消
  • 总结:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档