ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

TLPI 第32章 读书笔记:Threads: Thread Cancellation 笔记和练习博客总目录见开始读TLPI。通常多个线程会并行执行每个线程执行自己的任务直到它通过调用 pthread_exit() 终止或者从线程的启动函数返回。有时候取消线程可能会很有用也就是说向线程发送请求要求它立即终止。例如如果一组线程正在进行计算其中一个线程检测到错误情况需要其他线程终止这时就可能需要取消线程。或者在基于 GUI 的应用程序中可以提供一个取消按钮让用户终止后台线程正在执行的任务在这种情况下主线程控制 GUI 的线程需要告诉后台线程终止。在本章中我们将介绍 POSIX 线程的取消机制。32.1 Canceling a Threadpthread_cancel() 函数会向指定的线程发送取消请求。#includepthread.hintpthread_cancel(pthread_tthread);Returns0on success,or a positive error number on error32.2 Cancellation State and Type在发出取消请求后pthread_cancel() 会立即返回也就是说它不会等待目标线程终止。目标线程具体会发生什么以及何时发生取决于该线程的取消状态和类型如下一节所述。#includepthread.hintpthread_setcancelstate(intstate,int*oldstate);intpthread_setcanceltype(inttype,int*oldtype);Bothreturn0on success,or a positive error number on errorpthread_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 SUSv3accept()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 canceledListing 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() 的目的仅仅是作为一个取消点。如果在调用这个函数时存在未处理的取消请求那么调用的线程会被终止。#includepthread.hvoidpthread_testcancel(void);一个正在执行不包含取消点的代码的线程可以定期调用 pthread_testcancel()以确保它能及时响应由其他线程发出的取消请求。32.5 Cleanup Handlers如果一个有待取消操作的线程在到达取消点时就直接被终止那么共享变量和 Pthreads 对象例如互斥锁可能会处于不一致的状态这可能导致进程中剩余的线程产生错误结果、死锁或崩溃。为了解决这个问题线程可以建立一个或多个清理处理程序——如果线程被取消这些函数会自动执行。清理处理程序可以执行一些任务比如在线程终止前修改全局变量的值和解锁互斥锁。每个线程都可以有一个清理处理程序的栈。当线程被取消时清理处理程序会从栈顶开始依次执行也就是说最近建立的处理程序最先被调用然后是次近的依此类推。当所有清理处理程序都执行完毕线程就会终止。pthread_cleanup_push() 和 pthread_cleanup_pop() 函数分别用于在调用线程的清理处理程序栈中添加和移除处理程序。#includepthread.hvoidpthread_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 normally32.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 Summarypthread_cancel() 函数允许一个线程向另一个线程发送取消请求也就是请求目标线程终止。目标线程如何响应这个请求取决于它的可取消状态和类型。如果可取消状态目前设置为禁用请求会一直挂起直到可取消状态被设置为可用。如果可取消功能启用可取消类型决定目标线程何时响应请求。如果类型是延迟deferred那么当线程下次调用 SUSv3 指定的一系列作为取消点的函数时就会发生取消。如果类型是异步asynchronous取消可能在任何时候发生这很少有用。线程可以建立一个清理处理程序栈这些处理程序是程序员定义的函数如果线程被取消这些函数会自动被调用来执行清理工作例如恢复共享变量的状态或解锁互斥锁。Further information请参考第29.10节列出的更多信息来源。 本章无练习题
返回列表