news 2026/9/3 15:34:14

TLPI 第32章 读书笔记:Threads: Thread Cancellation

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TLPI 第32章 读书笔记:Threads: Thread Cancellation

笔记和练习博客总目录见:开始读TLPI。

通常,多个线程会并行执行,每个线程执行自己的任务,直到它通过调用 pthread_exit() 终止,或者从线程的启动函数返回。

有时候,取消线程可能会很有用,也就是说,向线程发送请求,要求它立即终止。例如,如果一组线程正在进行计算,其中一个线程检测到错误情况,需要其他线程终止,这时就可能需要取消线程。或者,在基于 GUI 的应用程序中,可以提供一个取消按钮,让用户终止后台线程正在执行的任务;在这种情况下,主线程(控制 GUI 的线程)需要告诉后台线程终止。

在本章中,我们将介绍 POSIX 线程的取消机制。

32.1 Canceling a Thread

pthread_cancel() 函数会向指定的线程发送取消请求。

#include<pthread.h>intpthread_cancel(pthread_tthread);Returns0on success,or a positive error number on error

32.2 Cancellation State and Type

在发出取消请求后,pthread_cancel() 会立即返回;也就是说,它不会等待目标线程终止。

目标线程具体会发生什么,以及何时发生,取决于该线程的取消状态和类型,如下一节所述。

#include<pthread.h>intpthread_setcancelstate(intstate,int*oldstate);intpthread_setcanceltype(inttype,int*oldtype);Bothreturn0on success,or a positive error number on error

pthread_setcancelstate() 函数将调用线程的可取消状态设置为 state 参数指定的值。这个参数可以是以下值之一:

  • PTHREAD_CANCEL_DISABLE
    线程不可被取消。如果收到取消请求,它会保持挂起状态直到可取消性被启用。
  • PTHREAD_CANCEL_ENABLE
    线程可以被取消。这是新创建线程的默认可取消状态。

💡 这里说的保持挂起状态,指的是请求,而非线程。

线程之前的可取消状态会返回到 oldstate 指向的位置。

如果我们对之前的可取消状态不感兴趣,Linux 允许将 oldstate 指定为 NULL。很多其他实现也是这样;不过,SUSv3 并没有规定这个功能,所以可移植的应用程序不能依赖它。我们应该总是为 oldstate 指定一个非 NULL 的值。

暂时禁用取消(PTHREAD_CANCEL_DISABLE)很有用,比如当一个线程正在执行一段必须完成所有步骤的代码时。

如果一个线程是可取消的(PTHREAD_CANCEL_ENABLE),那么对取消请求的处理方式取决于线程的可取消类型,该类型由调用 pthread_setcanceltype() 时的 type 参数指定。这个参数可以是以下值之一:

  • PTHREAD_CANCEL_ASYNCHRONOUS
    线程可能随时被取消(可能立即,但不一定)。异步取消通常不太常用,我们会把相关讨论留到第32.6节。
  • PTHREAD_CANCEL_DEFERRED
    取消操作会保持挂起状态,直到到达一个取消点(见下一节)。这是新创建线程的默认取消类型。我们会在后面的章节中进一步讲解延迟取消。

线程的先前可取消类型会返回到 oldtype 指向的位置。

和 pthread_setcancelstate() 的 oldstate 参数一样,许多实现,包括 Linux,允许 oldtype 指定为 NULL,如果我们对之前的可取消类型不感兴趣的话。但 SUSv3 并没有规定这个特性,所以可移植的应用程序不能依赖它。我们应该总是为 oldtype 指定一个非 NULL 的值。

当线程调用 fork() 时,子线程会继承调用线程的可取消类型和状态。当线程调用 exec() 时,新程序主线程的可取消类型和状态会被重置为 PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED。

32.3 Cancellation Points

当线程的可取消性处于启用且延迟模式时,取消请求仅在线程下一次抵达取消点时才会被响应。取消点是对具体实现所定义的一组函数中任一函数的调用。

《单一 UNIX 规范第 3 版》(SUSv3)明确规定:表 32-1 中列出的函数,只要在某实现中被提供,就必须作为取消点。这类函数大多具备让线程陷入无限期阻塞的特性。

Table 32-1: Functions required to be cancellation points by SUSv3

accept()nanosleep()sem_timedwait()
aio_suspend()open()sem_wait()
clock_nanosleep()pause()send()
close()poll()sendmsg()
connect()pread()sendto()
creat()pselect()sigpause()
fcntl(F_SETLKW)pthread_cond_timedwait()sigsuspend()
fsync()pthread_cond_wait()sigtimedwait()
fdatasync()pthread_join()sigwait()
getmsg()pthread_testcancel()sigwaitinfo()
getpmsg()putmsg()sleep()
lockf(F_LOCK)putpmsg()system()
mq_receive()pwrite()tcdrain()
mq_send()read()usleep()
mq_timedreceive()readv()wait()
mq_timedsend()recv()waitid()
msgrcv()recvfrom()waitpid()
msgsnd()recvmsg()write()
msync()select()writev()

除了表32-1中的函数外,SUSv3还指定了一组更大的函数集合,实施时可以将它们定义为取消点。这些包括stdio函数、dlopen API、syslog API、nftw()、popen()、semop()、unlink()以及各种从系统文件(比如utmp文件)获取信息的函数。可移植程序必须正确处理线程在调用这些函数时可能被取消的情况。

SUSv3 指出,除了必须和可能成为取消点的两个函数列表之外,标准中的其他函数都不能作为取消点(也就是说,一个可移植的程序不需要处理调用这些其他函数可能导致线程取消的情况)。

SUSv4 在必须的取消点函数列表中增加了 openat(),同时移除了 sigpause()(它被移到可能的取消点函数列表)和 usleep()(已从标准中删除)。

实现可以自由地将标准中未指定的附加函数标记为取消点。任何可能阻塞的函数(可能因为它需要访问文件)都是成为取消点的潜在候选。在 glibc 中,许多非标准函数就是因为这个原因被标记为取消点。

当收到取消请求时,如果线程的可取消性已启用且为延迟模式,它将在下次到达取消点时终止。如果线程没有分离,那么进程中的其他线程必须与它进行连接,以防止它成为僵尸线程。当一个被取消的线程被 join 时,传递给 pthread_join() 的第二个参数会返回一个特殊的线程返回值:PTHREAD_CANCELED。

Example program
清单32-1展示了使用pthread_cancel()的一个简单示例。主程序创建了一个执行无限循环的线程,每次循环都会休眠一秒并打印循环计数器的值。(这个线程只有在收到取消请求或者进程退出时才会终止。)与此同时,主程序休眠3秒,然后向它创建的线程发送取消请求。运行这个程序时,我们会看到如下情况:

$ ./thread_cancel New thread started Loop1Loop2Loop3Thread was canceled

Listing 32-1: Canceling a thread with pthread_cancel()

// threads/thread_cancel.c// 略。

32.4 Testing for Thread Cancellation

在清单32-1中,由main()创建的线程接受了取消请求,因为它执行了一个是取消点的函数(sleep() 是一个取消点;printf() 可能也是)。不过,假设一个线程执行的循环中没有任何取消点(例如,一个以计算为主的循环)。在这种情况下,线程永远不会响应取消请求。

pthread_testcancel() 的目的仅仅是作为一个取消点。如果在调用这个函数时存在未处理的取消请求,那么调用的线程会被终止。

#include<pthread.h>voidpthread_testcancel(void);

一个正在执行不包含取消点的代码的线程,可以定期调用 pthread_testcancel(),以确保它能及时响应由其他线程发出的取消请求。

32.5 Cleanup Handlers

如果一个有待取消操作的线程在到达取消点时就直接被终止,那么共享变量和 Pthreads 对象(例如互斥锁)可能会处于不一致的状态,这可能导致进程中剩余的线程产生错误结果、死锁或崩溃。为了解决这个问题,线程可以建立一个或多个清理处理程序——如果线程被取消,这些函数会自动执行。清理处理程序可以执行一些任务,比如在线程终止前修改全局变量的值和解锁互斥锁。

每个线程都可以有一个清理处理程序的栈。当线程被取消时,清理处理程序会从栈顶开始依次执行;也就是说,最近建立的处理程序最先被调用,然后是次近的,依此类推。当所有清理处理程序都执行完毕,线程就会终止。

pthread_cleanup_push() 和 pthread_cleanup_pop() 函数分别用于在调用线程的清理处理程序栈中添加和移除处理程序。

#include<pthread.h>voidpthread_cleanup_push(void(*routine)(void*),void*arg);voidpthread_cleanup_pop(intexecute);

pthread_cleanup_push() 函数将 routine 中指定地址的函数添加到调用线程的清理处理程序栈顶。routine 参数是一个指向具有以下形式的函数的指针:

voidroutine(void*arg){/* Code to perform cleanup */}

传递给 pthread_cleanup_push() 的 arg 值会在清理处理函数被调用时作为参数传入。这个参数类型是 void *,但通过适当的类型转换,也可以传入其他数据类型。

通常,只有在线程在特定代码段执行期间被取消时才需要清理操作。如果线程在未被取消的情况下到达该段代码的结尾,那么清理操作就不再需要。因此,每次调用 pthread_cleanup_push() 都会有对应的 pthread_cleanup_pop() 调用。这个函数会移除清理处理函数栈顶的函数。如果 execute 参数非零,处理函数也会被执行。这在我们希望即使线程未被取消也执行清理操作时非常方便。

虽然我们把 pthread_cleanup_push() 和 pthread_cleanup_pop() 描述为函数,但 SUSv3 允许它们被实现为宏,展开成分别包含开括号 ({) 和闭括号 (}) 的语句序列。并不是所有 UNIX 实现都是这样,但 Linux 和许多其他系统是这样的。这意味着每次使用 pthread_cleanup_push() 时,必须在同一个词法块中对应一个 pthread_cleanup_pop()。(在实现是这样做的系统上,在 pthread_cleanup_push() 和 pthread_cleanup_pop() 之间声明的变量将只在该词法作用域内有效。)例如,写如下代码就是不正确的:

pthread_cleanup_push(func,arg);...if(cond){pthread_cleanup_pop(0);}

作为一种编码上的便利,如果线程通过调用 pthread_exit() 终止(但不是简单 return),任何尚未弹出的清理处理程序也会自动执行。

Example program
列表 32-2 中的程序提供了一个使用清理处理程序的简单例子。主程序创建了一个线程,这个线程的第一个动作是分配一块内存,并将其位置存储在 buf 中,然后锁住互斥锁 mtx r。由于线程可能会被取消,它使用 pthread_cleanup_push() 来安装一个清理处理程序,该处理程序会使用 buf 中存储的地址。如果它被调用,清理处理程序会释放内存并解锁互斥锁。

然后线程进入一个循环,等待条件变量 cond 被触发。根据程序是否提供了命令行参数,这个循环会以两种方式之一结束:

  • 如果没有提供命令行参数,线程会被 main() 取消。
    在这种情况下,取消会发生在调用 pthread_cond_wait() 时,这也是表 32-1 中显示的取消点之一。作为取消的一部分,使用 pthread_cleanup_push() 设置的清理处理程序会被自动调用。

  • 如果提供了命令行参数,条件变量会在相关的全局变量 glob 首先被设为非零值后被发送信号。
    在这种情况下,线程会继续执行 pthread_cleanup_pop(),在给定非零参数的情况下,这也会导致清理处理程序被调用。

主程序会与已终止的线程合并,并报告该线程是被取消还是正常终止的。

Listing 32-2: Using cleanup handlers

// threads/thread_cleanup.c// 略。

如果我们在没有任何命令行参数的情况下调用清单 32-2 中的程序,那么 main() 会调用 pthread_cancel(),清理处理程序会自动调用,我们会看到以下内容:

$ ./thread_cleanup thread: allocated memory at 0x7f13dc000b60 main: about to cancel thread cleanup: freeing block at 0x7f13dc000b60 cleanup: unlocking mutex main: thread was canceled

如果我们用命令行参数调用程序,那么 main() 会把 glob 设置为 1 并发出条件变量信号,清理处理程序会被 pthread_cleanup_pop() 调用,然后我们会看到如下内容:

$ ./thread_cleanup s thread: allocated memory at 0x7f9e34000b60 main: about to signal condition variable thread: conditionwaitloop completed cleanup: freeing block at 0x7f9e34000b60 cleanup: unlocking mutex main: thread terminated normally

32.6 Asynchronous Cancelability

当线程被设置为异步可取消(可取消类型为 PTHREAD_CANCEL_ASYNCHRONOUS)时,它可以在任何时间被取消(也就是说,在任何机器语言指令时都可能被取消);取消的执行不会等到线程下次到达取消点才发生。

异步取消的问题在于,尽管清理处理程序仍然会被调用,但处理程序无法确定线程的状态。在列表32-2中的程序中,使用了延迟可取消类型,线程只有在执行pthread_cond_wait()调用时才能被取消,而这是唯一的取消点。到那时,我们知道buf已经被初始化为指向一块分配好的内存,并且互斥锁mtx已经被锁定。然而,如果是异步可取消,线程可能在任何时刻被取消;例如,在malloc()调用之前、在malloc()调用与锁定互斥锁之间,或者在锁定互斥锁之后。清理处理程序无法知道取消发生在何处,也无法精确知道需要执行哪些清理步骤。此外,线程甚至可能在malloc()调用期间被取消,这之后很可能会导致混乱(见第7.1.3节)。

作为一个通用原则,一个异步可取消的线程不能分配任何资源,也不能获取任何互斥锁、信号量或锁。这排除了使用大量库函数,包括大部分Pthreads函数。(SUSv3对pthread_cancel()、pthread_setcancelstate()和pthread_setcanceltype()除外,这些函数被明确要求是异步取消安全的;也就是说,实施必须确保它们可以安全地从一个异步可取消的线程调用。)换句话说,很少有情况下异步取消是有用的。其中一个情况是取消处于计算密集型循环中的线程。

32.7 Summary

pthread_cancel() 函数允许一个线程向另一个线程发送取消请求,也就是请求目标线程终止。

目标线程如何响应这个请求取决于它的可取消状态和类型。如果可取消状态目前设置为禁用,请求会一直挂起,直到可取消状态被设置为可用。如果可取消功能启用,可取消类型决定目标线程何时响应请求。如果类型是延迟(deferred),那么当线程下次调用 SUSv3 指定的一系列作为取消点的函数时就会发生取消。如果类型是异步(asynchronous),取消可能在任何时候发生(这很少有用)。

线程可以建立一个清理处理程序栈,这些处理程序是程序员定义的函数,如果线程被取消,这些函数会自动被调用来执行清理工作(例如,恢复共享变量的状态或解锁互斥锁)。

Further information
请参考第29.10节列出的更多信息来源。

💡 本章无练习题

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 15:33:20

大型赛事投票总出幺蛾子?稳定性 + 防刷 + 全流程避坑指南

参与过好几次大型赛事的投票环节&#xff0c;从城市少儿才艺大赛、行业十大人物评选到十万人级的品牌人气票选&#xff0c;踩过的坑太多了&#xff1a;有一次赛事刚到决赛投票高峰&#xff0c;1 小时内流量翻了 5 倍&#xff0c;页面直接崩了&#xff0c;选手和粉丝炸锅&#x…

作者头像 李华
网站建设 2026/9/3 15:32:45

Three.js 颜色管理:别让你的 3D 场景忽明忽暗、颜色发灰

Three.js 颜色管理&#xff1a;别让你的 3D 场景忽明忽暗、颜色发灰 原文出处&#xff1a;Three.js Manual – Color Management 本文基于官方手册「Color Management」章节整理&#xff0c;用通俗方式带你看懂&#xff1a;为什么同样是红色&#xff0c;贴进场景里就变暗了&…

作者头像 李华
网站建设 2026/9/3 15:32:35

从一张商品图到全套电商视觉内容:AI生成工作流实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 15:31:36

MinimaxH3音频驱动数字人:口型同步原理与ComfyUI本地部署实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 15:30:18

AI短剧自动化:从故事到成片的工程流水线

AI短剧自动化最容易被低估的地方&#xff0c;是它本质上不是一个大模型在“生成剧”&#xff0c;而是一条由文本理解、语音合成、视觉素材生成和视频剪辑串联起来的工程流水线。所谓“从故事到成片一键搞定”&#xff0c;在当前可实现方案里往往不是一条提示词就出片&#xff0…

作者头像 李华
网站建设 2026/9/3 15:27:33

从零构建AI Agent平台:核心架构、技术选型与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华