TLPI读书笔记第32章:线程取消
有时候,需要将一个线程取消(cancel)。亦即,向线程发送一个请求,要求其立即退出。比如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其他线程退出,取消线程的功能这时就派上用场。还有一种情况,一个由图形用户界面(GUI)驱动的应用程序可能会提供一个“取消”按钮,以便用户可以终止后台某一线程正在执行的任务。这种情况下,主线程(控制图形用户界面)需要请求后台线程退出。本章就来讨论 POSIX 线程的取消机制。
32.1 取消一个线程
函数 pthread_cancel()向由 thread 指定的线程发送一个取消请求。
发出取消请求后,函数 pthread_cancel()当即返回,不会等待目标线程的退出。 准确地说,目标线程会发生什么?何时发生?这些都取决于下节将要述及的线程取消状态(state)和类型(type)。
32.2 取消状态及类型
函数 pthread_setcancelstate()和 pthread_setcanceltype()会设定标志, 允许线程对取消请求的响应过程加以控制。
函数 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 给定。参数 type 有如下值:
PTHREAD_CANCEL_ASYNCHRONOUS 可能会在任何时点(也许是立即取消,但不一定)取消线程。
异步取消的应用场景很少,将延后至 32.6 节再做讨论。
PTHREAD_CANCEL_DEFERED 取消请求保持挂起状态,直至到达取消点(cancellation point,见下节)。这也是新建线程的缺省类型。后续各节将介绍延迟取消(deferred cancelability)的更多细节。线程原有的取消类型将返回至参数 oldtype 所指向的位置。
当某线程调用 fork()时,子进程会继承调用线程的取消性类型及状态。而当某线程调用exec()时,会将新程序主线程的取消性类型及状态分别重置为 PTHREAD_ CANCEL_ ENABLE和 PTHREAD_CANCEL_DEFERRED。
32.3 取消点
若将线程的取消性状态和类型分别置为启用和延迟,仅当线程抵达某个取消点(cancellation point)时,取消请求才会起作用。取消点即是对由实现定义的一组函数之一加以调用。 SUSv3 规定,实现若提供了表 32-1 中所列的函数,则这些函数必须是取消点。其中的大部分函数都有能力将线程无限期地堵塞起来。
除表 32-1 所列函数之外, SUSv3 还指定了大量函数,系统实现可以将其定义为取消点。其中包括 stdio 函数、 dlopen API、 syslog API、 nftw()、 popen()、 semop()、 unlink(),以及从诸如 utmp 之类的系统文件中获取信息的各种函数。可移植应用程序必须正确处理这一情况:线程在调用这些函数时有可能遭到取消。
SUSv3 规定,除了上述两组必须或可能是可取消点的函数之外,不得将标准中的任何其他函数视为取消点(亦即,调用这些函数不会招致线程取消,可移植程序无需加以处理)。
SUSv4 在必须的可取消点函数列表中增加了 openat(),并移除了函数 sigpause()(将其移至“可能的”取消点函数列表中)和函数 usleep()(已从标准中删除)。
线程一旦收到取消请求,且启用了取消性状态并将类型置为延迟,则其会在下次抵达取消点时终止。如果该线程尚未分离( not detached),那么为防止其变为僵尸线程,必须由其他线程对其进行连接( join)。连接之后,返回至函数 pthread_join()中第二个参数的将是一个特殊值: PTHREAD_CANCELED。
32.4 线程可取消性的检测
在程序清单 32-1 中,由 main()创建的线程会执行到属于取消点的函数( sleep()属于取消点, printf()可能也是),因而会接受取消请求。不过,假设线程执行的是一个不含取消点的循环(计算密集型[ compute-bound]循环),这时,线程永远也不会响应取消请求。
函数 pthread_testcancel()的目的很简单,就是产生一个取消点。线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。
当线程执行的代码未包含取消点时,可以周期性地调用 pthread_testcancel(),以确保对其他线程向其发送的取消请求做出及时响应。
32.5 清理函数(cleanup handler)
一旦有处于挂起状态的取消请求,线程在执行到取消点时如果只是草草收场,这会将共享变量以及 Pthreads 对象(例如互斥量)置于一种不一致状态,可能导致进程中其他线程产生错误结果、死锁,甚至造成程序崩溃。为规避这一问题,线程可以设置一个或多个清理函数,当线程遭取消时会自动运行这些函数,在线程终止之前可执行诸如修改全局变量,解锁互斥量等动作。 每个线程都可以拥有一个清理函数栈。当线程遭取消时,会沿该栈自顶向下依次执行清理函数,首先会执行最近设置的函数,接着是次新的函数,以此类推。当执行完所有清理函数后,线程终止。 函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈添加和移除清理函数。
pthread_cleanup_push()会将参数 routine 所含的函数地址添加到调用线程的清理函数栈顶。参数 routine 是一个函数指针,格式如下: 执行 pthread_cleanup_push()时给定的 arg 值,会作为调用清理函数时的参数。其参数类型为 void*,如果强制装换使用得当,那么通过该参数可以传入各种类型的数据。 通常,线程如在执行一段特殊代码时遭到取消,才需要执行清理动作。如果线程顺利执行完这段代码而未遭取消,那么就不再需要清理。所以,每个对 pthread_cleanup_push()的调用都会伴随着对 pthread_cleanup_pop()的调用。此函数从清理函数栈中移除最顶层的函数。如果参数 execute 非零,那么无论如何都会执行清理函数。在函数未遭取消而又希望执行清理动作的情况下,这会非常方便。 尽管这里把 pthread_cleanup_push()和 pthread_cleanup_pop()描述为函数, SUSv3 却允许将它们实现为宏(macro),可展开为分别由{和}所包裹的语句序列。并非所有的 UNIX 都这样做,不过包括 Linux 在内的很多系统都是使用宏来实现的。这意味着, pthread_ cleanup_push()和与其配对的 pthread_cleanup_pop()属于同一个语法块,必须一一对应。(一旦以此方式来实现pthread_cleanup_push()和 pthread_cleanup_pop(),在对两者的调用间所声明的变量,其作用域将受限于这一语法块。)例如,以下代码就不正确: 为便于编码,若线程因调用 pthread_exit()而终止,则也会自动执行尚未从清理函数栈中弹出(pop)的清理函数。线程正常返回(return)时不会执行清理函数。
32.6 异步取消
如果设定线程为可异步取消时(取消性类型为PTHREAD_CANCEL_ASYNCHRONOUS),可以在任何时点将其取消(亦即,执行任何机器指令时),取消动作不会拖延到下一个取消点才执行。 异步取消的问题在于,尽管清理函数依然会得以执行,但处理函数却无从得知线程的具体状态。程序清单 32-2 采用了延时取消类型,只有在执行到 pthread_cond_wait()这一唯一的取消点时,线程才会遭到取消。此时可知,已将 buf 初始化为指向新分配的内存块,并且锁定了互斥量 mtx。不过,要是采用异步取消,就可以在任意点取消线程(例如,调用 malloc()之前,调用 malloc()与锁定互斥量之间,或者锁定互斥量之后)。清理函数无法知道将在哪里发生取消动作,或者准确地来说,清理函数不清楚需要执行哪些清理步骤。此外,线程也很可能在对 malloc()的调用期间被取消,这极有可能造成后续的混乱(见 7.1.3 节)。
作为一般性原则,可异步取消的线程不应该分配任何资源,也不能获取互斥量或锁。这导致大量库函数无法使用,其中就包括 Pthreads 函数的大部分。 ( SUSv3 中有 3 处例外pthread_cancel()、 pthread_setcancelstate()以及 pthread_setcanceltype(),规范明确要求将它们实现为“异步取消安全(async-cancel-safe)”,亦即,实现必须确保在可异步取消的线程中可以安全调用它们。)换言之,异步取消功能鲜有应用场景,其中之一就是:取消在执行计算密集型循环的线程
32.7 总结
函数 pthread_cancel()允许某线程向另一个线程发送取消请求,要求目标线程终止。目标线程如何响应,取决于其取消性状态和类型。如果禁用线程的取消性状态,那么请求会保持挂起(pending)状态,直至将线程的取消性状态置为启用。如果启用取消性状态,那么线程何时响应请求则依赖于取消性类型。若类型为延迟取消,则在线程下一次调用某个取消点(由 SUSv3 标准所规定的一系列函数之一)时,取消发生。如果为异步取消类型,取消动作随时可能发生(鲜有使用)。 线程可以设置一个清理函数栈,其中的清理函数属于由开发人员定义的函数,当线程遭到取消时,会自动调用这些函数以执行清理工作(例如,恢复共享变量状态,或解锁互斥量)。