终止线程

当线程从它的入口点例程返回时将自动终止。

线程还可以使用称为取消的机制来明确地终止它自身或终止进程中的任何其他线程。 由于所有的线程都共享同样的数据空间,所以线程必须在终止时执行清除操作;线程库提供 清除处理程序用于此目的。

退出线程

当线程调用 exit 子例程时,进程可以随时退出。 类似地,线程可以通过调用 pthread_exit 子例程随时退出。

调用 exit 子例程会终止整个进程,包括它所有的线程。 在多线程程序中,需要终止整个进程时应该只使用 exit 子例程;例如,在发生不可恢复错误的情况下。 即使在退出初始线程 时,pthread_exit 子例程也应该是首选的。

调用 pthread_exit 子例程会终止调用线程。 status 参数由库保存并且在与已终止的线程连接时可以进一步使用。 调用 pthread_exit 子例程与此相似但不完全相同,它从线程的初始例程返回。 从线程的初始例程返回的结果取决于该线程:

  • 从初始线程返回将显式地调用 exit 子例程,这将终止进程中所有的线程。
  • 从另一个线程返回将显式地调用 pthread_exit 子例程。 返回值与 pthread_exit 子例程的 status 参数具有相同的角色。

要避免显示地调用 exit 子例程,请使用 pthread_exit 子例程退出线程。

退出初始线程(例如,从 main 例程调用 pthread_exit 子例程)将不会终止进程。 它只终止初始线程。 如果初始线程被终止,那么进程中的最后一个线程终止时该进程也将终止。 在这种情况下,进程返回码是 0。

以下程序用每种语言正好显示 10 条消息。 这是通过在创建两个线程后,调用 main 例程中的 pthread_exit 子例程,并在 Thread 例程中创建一个循环完成的。

#include <pthread.h>    /* include file for pthreads - the 1st */
#include <stdio.h>      /* include file for printf()           */

void *Thread(void *string)

{
        int i;
 
        for (i=0; i<10; i++)
                printf("%s\n", (char *)string);
        pthread_exit(NULL);
}

int main()
{
        char *e_str = "Hello!";
        char *f_str = "Bonjour !";
 
        pthread_t e_th;
        pthread_t f_th;
 
        int rc;
 
        rc = pthread_create(&e_th, NULL, Thread, (void *)e_str);
        if (rc)
                exit(-1);
        rc = pthread_create(&f_th, NULL, Thread, (void *)f_str);
        if (rc)
                exit(-1);
        pthread_exit(NULL);
}

pthread_exit 子例程发布特定于线程的所有数据,包括线程的堆栈。 分配到该堆栈上的所有 数据都变为无效,因为该堆栈已被释放并且相应的内存可能已被另一个线程重新使用。 所以,在线程调用 pthread_exit 子例程之前,必须清除分配到线程的堆栈上的线程同步对象(互斥对象和条件变量)。

exit 子例程不同,pthread_exit 子例程不会清除在线程之间共享的系统资源。 例如,pthread_exit 子例程不会关闭文件,因为其他进程可能使用这些文件。

取消线程

线程取消机制使得线程可以受控方式终止进程中任何其他线程的执行。 对取消通知进行操作时,目标线程(即要取消的线程)可以用多种方式保持取消请求处于暂挂状态并执行特定于应用程序的清除处理。 取消后,线程显示地 调用 pthread_exit((void *)-1) 子例程。

通过调用 pthread_cancel 子例程来请求取消线程。 调用返回时,已经注册请求但是线程可能仍在运行。 仅当指定的线程标识无效时,对 pthread_cancel 子例程 的调用才会失败。

可取消状态和类型

线程的可取消 (cancelability) 状态和类型决定接收取消请求时采取的操作。 每个线程使用 pthread_setcancelstatepthread_setcanceltype 子例程来控制自己的可取消状态和类型。

以下可能的可取消状态和可取消类型会导致三种可能的情况,如下表所示。
可取消状态 可取消类型 结果情况
已禁用 任意(忽略类型) 禁用可取消
已启用 延迟 延迟可取消
已启用 异步 异步可取消
可能的情况如下所示:
  • 禁用可取消。 任何取消请求都设为暂挂,直到可取消状态被更改或线程以另一种方式终止为止。

    仅当线程执行无法中断的操作时才应该禁用可取消。 例如,如果线程正在执行某些复杂的文件保存操作(例如已建立索引的数据库)并 且在操作期间取消该操作,文件可能会处于不一致的状态。 要避免这种情况,线程应该在文件保存操作期间禁用可取消。

  • 延迟可取消。 任意取消请求都设为暂挂,直到线程到达下一个取消点。 这是缺省的可取消状态。

    该可取消状态确保线程可以被取消,但是限制线程执行期间的特定时刻的取消操作,称为取消点。 在取消点上取消的线程使系统处于安全状态;然而,用户数据可能会不一致或者已取消的线程可能会保持锁定该数据。 要避免这些情况,请使用清除处理 程序或在关键区域内禁用可取消。 有关更多信息,请参阅 使用清除处理程序

  • 异步取消。 将对任何取消请求立即响应。

    占有资源的线程以异步方式取消时将会导致进程,或者甚至是系统处于很难和不可能恢复的状态。 有关 async-cancel 安全性的更多信息,请参阅 异步-取消安全性

异步取消安全

如果一个函数被编写为启用异步可取消时,即使用任何指令传递取消 请求,对函数进行调用也不会导致任何资源被损坏,那么该函数称为异步取消安全

任何在获取资源时会产生副作用的函数都无法实现异步取消安全。 例如,如果在启用异步可取消的情况下调用 malloc 子例程,那么它可能会成功获取资源,但是在返回到调用者时,它可以对取消请求执行操作。 在这种情况下,程序无法知道是否获取了资源。

出于此原因,不认为大多数库例程是异步取消安全的。 建议仅当确定执行不需要拥有资源的操作以及仅在调用 已经是异步取消安全的库例程时才使用异步可取消。

以下子例程是异步取消安全的;它们确保即使启用异步可取消,也将正确处理取消:

  • pthread_cancel
  • pthread_setcancelstate
  • pthread_setcanceltype

异步可取消的一个替代方法是使用延迟可取消并通过调用 pthread_testcancel 子例程添加显式取消点。

取消点

取消点是特定子例程中的点,如果启用延迟可取消,那么在这些点线程必须对任何正在暂挂的取消请求执行操作。 所有这些子例程都可能阻塞对线程的调用或无限期地计算。

也可以通过调用 pthread_testcancel 子例程来创建显式取消点。 该子例程只创建一个取消点。 如果启用延迟可取消,并且有一个取消请求正在暂挂,那么对该请求执行操作并终止线程。 否则,子例程仅仅返回而已。

调用以下子例程时还将出现其他取消点:

  • pthread_cond_wait
  • pthread_cond_timedwait
  • pthread_join

pthread_mutex_lockpthread_mutex_trylock 子例程不提供取消点。 如果它们提供取消点的话,那么调用这些子例程(许多函数都这样)的所有函数都将提供一个取消点。 有太多取消点将使得编程变得十分困难,因为这需要大量 对可取消的禁用和恢复,或者要尝试在每个可能的地方安排进行可靠的清除的额外工作。 有关这些子例程的更多信息,请参阅 使用互斥对象

当线程执行以下函数时将出现取消点:

函数
暂停 CLOSE
creat fcntl
fsync 获取消息
getpmsg 锁定
mq_receive mq_send
msgrcv msgsnd
毫同步 纳米睡眠
OPEN PAUSE
轮询 (poll) 前言
pthread_cond_timedwait pthread_cond_wait
pthread_join pthread_testcancel
输出消息 pwrite
READ
SELECT sem_wait
西格暂停 sigsuspend
sigtimedwait sigwait
sigwaitinfo sleep
系统 tc漏极
我们睡觉 WAIT
wait3 放弃
waitpid WRITE
writev

线程执行以下函数时也将出现取消点:

函数函数函数
捕获 catgets 猫开
关闭目录 closelog ctermid
dbm_close dbm_delete dbm_fetch
dbm_nextkey dbm_open dbm_store
关闭 dlopen endgrent
endpwent fwprintf fwrite
fwscanf getc getc_unlocked
getchar getchar_unlocked getcwd
getdate getgrent getgrgid
getgrgid_r getgrnam getgrnam_r
getlogin getlogin_r 弹出
printf 输出 putc_unlocked
输入字符 putchar_unlocked 放置
pututxline putw putwc
putwchar readdir readdir_r
移除 rename REWIND
endutxent 关闭 fcntl
冲洗 fgetc fgetpos
fgets fgetwc fgetws
fopen fprintf fputc
fput getpwent getpwnam
getpwnam_r getpwuid getpwuid_r
获取 getutxent getutxid
getutxline getw getwc
getwchar getwd 恢复目录
scanf 种子目录 semop
setgrent setpwent setutxent
strerror syslog tmpfile
tmpnam ttyname ttyname_r
fputwc fputws fread
自由打开 fscanf fseek
fseeko fsetpos ftell
ftello ftw glob
图标关闭 iconv_open ioctl
lseek mkstemp nftw
opendir openlog 关闭
错误 未获取 取消 getwc
取消链接 vfprintf vfwprintf
vprintf vwprintf wprintf
wscanf

在函数调用期间发生暂挂时对取消请求执行操作的副作用与单线程程序中对函数的调用被信号打断,并且给定函数返回 [EINTR] 时所见的副作用一样。 在调用任何取消清除处理程序之前都会发生这种副作用。

如果线程启用了取消并以该线程为目标进行了取消请求,并且线程调用 pthread_testcancel 子例程,那么 在 pthread_testcancel 子例程返回之前将对取消请求执行操作。 如果线程启用了可取消,而且线程有一个异步取消请求 正在暂挂并且该线程在等待事件发生的取消点暂挂,那么对该取消请求执行操作。 然而,如果线程在取消点暂挂,并且在对取消请求执行操作之前线程等待 的事件发生,那么事件序列将确定是对取消请求执行操作还是让请求保持暂挂并且线程恢复正常执行。

取消示例

在以下示例中,两个“writer”线程都在 10 秒钟后并且将消息写入至少 5 次后被取消。

#include <pthread.h>    /* include file for pthreads - the 1st */
#include <stdio.h>      /* include file for printf()           */
#include <unistd.h>     /* include file for sleep()            */
void *Thread(void *string)
{
        int i;
        int o_state;
 
        /* disables cancelability */
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &o_state);
 
        /* writes five messages */
        for (i=0; i<5; i++)
                printf("%s\n", (char *)string);
 
        /* restores cancelability */
        pthread_setcancelstate(o_state, &o_state);
 
        /* writes further */
        while (1)
                printf("%s\n", (char *)string);
        pthread_exit(NULL);
}
int main()
{
        char *e_str = "Hello!";
        char *f_str = "Bonjour !";
 
        pthread_t e_th;
        pthread_t f_th;
 
        int rc;
 
        /* creates both threads */
        rc = pthread_create(&e_th, NULL, Thread, (void *)e_str);
        if (rc)
                return -1;
        rc = pthread_create(&f_th, NULL, Thread, (void *)f_str);
        if (rc)
                return -1;
  
        /* sleeps a while */
        sleep(10);
 
        /* requests cancelation */
        pthread_cancel(e_th);
        pthread_cancel(f_th);
 
        /* sleeps a bit more */
        sleep(10);
        pthread_exit(NULL);
}

计时器和休眠子例程

计时器例程在调用线程的上下文中执行。 因此,如果计时器到期,那么在线程的上下文中调用看守程序计时器函 数。 当进程或线程休眠时,它们将不使用处理器。 在多线程的进程中,只会使调用线程休眠。

使用清除处理程序

当线程终止时,清除处理程序提供了一种用于发布资源和恢复不变量的可移植机制。

调用清除处理程序

清除处理程序是特定于每个线程的。 一个线程可以有多个清除处理程序;它们存储在特定于线程的 LIFO(后进先出)堆栈中。 在以下情况中调用清除处理程序:

  • 线程从入口点例程返回。
  • 线程调用 pthread_exit 子例程。
  • 线程对取消请求执行操作。

清除处理程序由 pthread_cleanup_push 子例程推送到清除堆栈。 pthread_cleanup_pop 子例程从堆栈中弹出最顶级的清除处理程序,并选择性地执行该处理程序。 不再需要清除处理程序时使用此子例程。

清除处理程序是用户定义的例程。 它有一个参数和一个空指针,调用 pthread_cleanup_push 子例程时指定它们。 可以指定一个指向某些数据的指针,清除处理程序在执行其操作时需要这些数据。

在以下示例中,为执行某个操作分配了一个缓冲区。 启用延迟可取消后,该操作可以在任意取消点停止。 在此示例 中,建立了一个清除处理程序来释放缓冲区。

/* the cleanup handler */
 
cleaner(void *buffer)
 
{
        free(buffer);
}

/* fragment of another routine */
...
myBuf = malloc(1000);
if (myBuf != NULL) {
        
        pthread_cleanup_push(cleaner, myBuf);
 
        /*
         *       perform any operation using the buffer,
         *       including calls to other functions
         *       and cancelation points
         */
        
        /* pops the handler and frees the buffer in one call */
        pthread_cleanup_pop(1);
}

使用延迟可取消将确保在缓冲区分配和清除处理程序注册期间线程不会对任何取消请求执行操作,因为 malloc 子例程或 pthread_cleanup_push 子例程都不提供取消点。 弹出清除处理 程序时,将执行处理程序并释放缓冲区。 弹出处理程序时,更复杂的程序可能不会执行它,因为清除处理程序被认为是代码的受保护部分的“紧急出口”。

均衡进栈操作和出栈操作

pthread_cleanup_pushpthread_cleanup_pop 子例程 在相同的词法作用域中应该始终成对出现;即,在相同的函数和相同的语句块中。 它们可以看作是左右括号,将代码的受保护部分括起来。

此规则的原因是在某些系统上,这些子例程实现为宏。 pthread_cleanup_push 子例程实现为左花括号,后跟其他语句:
#define pthread_cleanup_push(rtm,arg) { \
         /* other statements */
pthread_cleanup_pop 子例程实现为右花括号,后面跟随其他语句:
#define pthread_cleanup_pop(ex) \
         /* other statements */  \
}

遵循用于 pthread_cleanup_pushpthread_cleanup_pop 子 例程的平衡规则可以避免移植到其他系统时编译器错误或程序的意外行为。

在 "AIX中,"pthread_cleanup_push和 "pthread_cleanup_pop子程序是库例程,可以在同一语句块内不平衡。 然而,在程序中它们必须是平衡 的,因为清除处理程序是堆栈化的。

子例程 描述
pthread_attr_destroy 删除线程属性对象。
pthread_attr_get拆卸状态 返回线程属性对象的 detachstate 属性的值。
pthread_attr_init 创建一个线程属性对象并使用缺省值进行初始化。
pthread_cancel 请求取消线程。
pthread_cleanup_pop 除去并有选择地执行调用线程的清除堆栈顶端的例程。
pthread_cleanup_push 将例程压入调用线程的清除堆栈中。
pthread_create 创建一个新线程,初始化它的属性并使它可以运行。
pthread_equal 比较两个线程标识。
pthread_exit 终止调用线程。
pthread_self 返回调用线程的标识。
pthread_setcancelstate 设置调用线程的可取消状态。
pthread_setcanceltype 设置调用线程的可取消类型。
pthread_testcancel 在调用线程中创建取消点。