多线程编程指南

扩展传统信号

传统的 UNIX 信号模型通过相当自然的方式扩展到线程。关键特征是信号是在进程范围内部署的,而信号掩码是针对每个进程部署的。信号的进程范围部署是使用传统的机制(signal(3C)、sigaction(2) 等)建立的。

当信号处理程序标记为 SIG_DFLSIG_IGN 时,将对整个接收进程执行信号接收操作。 这些信号包括退出、核心转储、停止、继续和忽略。系统将针对进程中的所有线程执行这些信号的接收操作。因此,不存在哪个线程拾取信号的问题。退出、核心转储、停止、继续和忽略信号都没有处理程序。有关信号的基本信息,请参见 signal(5)

每个线程都有自己的信号掩码。当线程使用的内存或状态同时也被信号处理程序使用时,可通过信号掩码来阻塞某些信号。进程中的所有线程都共享由 sigaction(2) 及其变体设置的一组信号处理程序。

一个进程中的线程不能将信号发送到另一个进程中的特定线程。通过 kill(2)、sigsend(2) 或 sigqueue(3RT) 发送到进程的信号由该进程中任何接收线程来处理。

信号被分为以下类别:陷阱、异常和中断。异常是以同步方式生成的信号。陷阱和中断是以异步方式生成的信号。

就像在传统的 UNIX 中一样,如果信号处于暂挂状态,则该信号的其他实例通常没有其他影响。暂挂信号由位表示,而不是由计数器表示。但是,通过 sigqueue(3RT) 接口发送的信号允许在进程中对同一信号的多个实例排队。

对于单线程进程而言,线程接收信号时如果被阻塞在系统调用中,则该线程可能很早就会返回。如果线程很早就返回,则该线程会返回 EINTR 错误代码,或在 I/O 调用中传输的字节数比请求的字节数要少。

对多线程程序特别重要的一点就是信号对 pthread_cond_wait(3C) 产生的影响。此调用通常仅针对 pthread_cond_signal(3C)pthread_cond_broadcast(3C) 返回零,而不会出现任何错误。但是,如果等待线程接收传统的 UNIX 信号,则 pthread_cond_wait() 将返回零,即使唤醒是虚假的也是如此。在这种情况下,Solaris 线程 cond_wait(3C) 函数将返回 EINTR。有关更多信息,请参见中断对条件变量的等待

同步信号

陷阱(如 SIGILLSIGFPESIGSEGV)是由于对线程执行操作引起的,如除以零或引用不存在的内存。陷阱仅由导致陷阱的线程处理。进程中的多个线程可以同时生成和处理同种类型的陷阱。

可以很容易地针对同时生成的信号将信号扩展到各个线程。可以针对生成同步信号的线程调用处理程序。

但是,如果进程选择不建立相应的信号处理程序,则出现陷阱时将执行缺省操作。即使针对生成的信号阻塞违例线程,也会执行缺省操作。这类信号的缺省操作是终止进程,可能还会进行核心转储。

这类同步信号通常意味着整个进程出现严重问题,而不仅仅是线程出现问题。在这种情况下,终止进程通常是很好的选择。

异步信号

中断(如 SIGINTSIGIO)与任何线程都是异步的,而且是由进程外的某些操作引起的。这些中断可能是其他进程显式发送的信号,也可能代表外部操作(如用户键入 Ctrl-C 组合键)。

中断可由信号掩码允许中断的任何线程来处理。即使有多个线程可以接收中断,也只能选择一个线程。

将同一信号的多个实例发送到进程时,每个实例都可由单独的线程来处理。但是,可用线程不得屏蔽信号。当所有的线程都屏蔽信号时,信号将被标记为暂挂,而由取消屏蔽信号的第一个线程来处理信号。

延续语义

延续语义是传统的处理信号的方式。信号处理程序返回时,将控制恢复进程在中断时所处的位置。此控制恢复非常适合于单线程进程中的异步信号,如示例 5–1 所示。

在其他编程语言(如 PL/1)中,此控制恢复还用作异常处理机制。


示例 5–1 延续语义

unsigned int nestcount;



unsigned int A(int i, int j) {

    nestcount++;



    if (i==0)

        return(j+1)

    else if (j==0)

        return(A(i-1, 1));

    else

        return(A(i-1, A(i, j-1)));

}



void sig(int i) {

    printf("nestcount = %d\n", nestcount);

}



main() {

    sigset(SIGINT, sig);

    A(4,4);

}

对信号执行的操作

本节介绍对信号执行的操作。

设置线程的信号掩码

将信号发送到特定线程

等待指定信号

在给定时间内等待指定的信号

设置线程的信号掩码

pthread_sigmask(3C) 对线程执行 sigprocmask(2) 对进程所执行的操作。pthread_sigmask() 可以设置 thread 的信号掩码。创建新的线程时,其初始掩码是从其创建者继承的。

在多线程进程中对 sigprocmask() 执行调用等效于对 pthread_sigmask() 执行调用。有关更多信息,请参见 sigprocmask(2) 手册页。

将信号发送到特定线程

pthread_kill(3C)kill(2) 的线程模拟。pthread_kill() 可以将信号发送到特定线程。发送到指定线程的信号不同于发送到进程的信号。将信号发送到进程时,信号可由该进程中的任何线程来处理。通过 pthread_kill() 发送的信号只能由指定线程来处理。

可以使用 pthread_kill() 将信号仅发送到当前进程中的线程。由于 thread_t 类型的线程标识符的范围是本地,因此不能指定当前进程范围以外的线程。

通过目标线程接收信号时,调用的操作(处理程序 SIG_DFLSIG_IGN)通常为全局操作。如果将 SIGXXX 发送到线程,且 SIGXXX 的作用是中止进程,则目标线程接收信号时将中止整个进程。

等待指定信号

对于多线程程序,sigwait(2) 是可供使用的首选接口,因为 sigwait() 可以很好地处理异步生成的信号。

sigwait() 将导致调用线程等待,直到由其设置参数标识的任何信号被传送到该线程为止。线程等待的同时,系统将取消屏蔽由设置参数标识的信号,但调用返回时将恢复原始掩码。

由设置参数标识的所有信号必定会在所有线程(包括调用线程)上受到阻塞。否则,sigwait() 可能无法正常工作。

使用 sigwait() 将线程与异步信号分离。创建一个侦听异步信号的线程时,可同时创建其他线程来阻塞为此进程设置的任何异步信号。

从 Solaris 2.5 发行版开始,可以使用 sigwait() 的两个版本:Solaris 2.5 版本和 POSIX 标准版本。新的应用程序和新的库应该使用 POSIX 标准接口,因为 Solaris 版本在未来的发行版中可能不可用。

以下示例显示了 sigwait() 的两个版本的语法:

#include <signal.h>



/* the Solaris 2.5 version*/

int sigwait(sigset_t *set);



/* the POSIX standard version */

int sigwait(const sigset_t *set, int *sig);

传送信号时,POSIX sigwait() 将清除暂挂信号,并将信号数字置于 sig 中。许多线程可以同时调用 sigwait(),但是针对每个接收的信号仅返回一个线程。

借助 sigwait(),可以同时处理异步信号。信号到达后,处理这类信号的线程将立即调用 sigwait() 并返回。通过确保所有线程(包括 sigwait() 的调用程序)都屏蔽异步信号,可确保信号仅由预期处理程序处理,且安全地进行处理。

通过始终屏蔽所有线程中的所有信号并在必要时调用 sigwait(),可以使应用程序中依赖于信号的线程的安全性大大提高。

通常,可以创建一个或多个为等待信号而调用 sigwait() 的线程。由于 sigwait() 甚至会检索屏蔽的信号,因此一定要阻塞所有其他线程中的重要信号,以便不会意外传送这些信号。

信号到达时,线程将从 sigwait() 返回,处理信号,并再次调用 sigwait() 以等待更多信号。信号处理线程并不限于使用异步信号安全函数。信号处理线程可采用通常的方式与其他线程同步。MT 接口安全级别定义了异步信号安全类别。


注 –

sigwait() 不能接收同步生成的信号。


在给定时间内等待指定的信号

sigtimedwait(3RT) 类似于 sigwait(2),但在指定的时间内没有收到信号时,sigtimedwait() 将失败并返回错误。

定向于线程的信号

借助定向于线程的信号的概念,对 UNIX 信号机制得到了扩展。定向于线程的信号就像普通的异步信号一样,但定向于线程的信号是被发送到特定线程,而不是进程。

与安装用于处理信号的信号处理程序相比,使用等待异步信号的单独线程可能更安全且更简单。

一种处理异步信号的更好方式是同步处理这些信号。通过调用 sigwait(2),线程可以一直等待,直到信号出现为止。请参见等待指定信号


示例 5–2 异步信号和 sigwait(2)

main() {

    sigset_t set;

    void runA(void);

    int sig;



    sigemptyset(&set);

    sigaddset(&set, SIGINT);

    pthread_sigmask(SIG_BLOCK, &set, NULL);

    pthread_create(NULL, 0, runA, NULL, PTHREAD_DETACHED, NULL);



    while (1) {

        sigwait(&set, &sig);

        printf("nestcount = %d\n", nestcount);

        printf("received signal %d\n", sig);

    }

}



void runA() {

    A(4,4);

    exit(0);

}

本示例将修改示例 5–1 的代码。主例程将屏蔽 SIGINT 信号,创建一个子线程(用于调用前一个示例的函数 A),并发出 sigwait() 来处理 SIGINT 信号。

请注意,信号在计算线程中将被屏蔽,因为计算线程将从主线程继承其信号掩码。当且仅当主线程在 sigwait() 内部不受阻塞时,才能受到保护,而不去处理 SIGINT

另外,请注意,使用 sigwait() 时不存在中断系统调用的危险。

完成语义

另一种处理信号的方式是使用完成语义。

当信号指明发生灾难性情况,导致没有理由继续执行当前代码块时,请使用完成语义。信号处理程序将代替其余有问题的块运行。换句话说,信号处理程序将完成该块。

示例 5–3 中,所讨论的块是 if 语句的 then 部分的主体。对 setjmp(3C) 的调用会在 jbuf 中保存程序的当前寄存器状态并返回 0,从而执行块。


示例 5–3 完成语义

sigjmp_buf jbuf;

void mult_divide(void) {

    int a, b, c, d;

    void problem();



    sigset(SIGFPE, problem);

    while (1) {

        if (sigsetjmp(&jbuf) == 0) {

            printf("Three numbers, please:\n");

            scanf("%d %d %d", &a, &b, &c);

            d = a*b/c;

            printf("%d*%d/%d = %d\n", a, b, c, d);

        }

    }

}



void problem(int sig) {

    printf("Couldn't deal with them, try again\n");

    siglongjmp(&jbuf, 1);

}

如果出现 SIGFPE 浮点异常,则系统将调用信号处理程序。

信号处理程序将调用 siglongjmp(3C)(用于恢复 jbuf 中保存的寄存器状态),进而导致程序再次从 sigsetjmp() 返回。保存的寄存器包括程序计数器和栈指针。

但是,此时 sigsetjmp(3C) 将返回 siglongjmp() 的第二个参数(值为 1)。请注意,块将被跳过,而仅在下一次迭代 while 循环期间执行。

可以在多线程程序中使用 sigsetjmp(3C)siglongjmp(3C)。请注意,一个线程永远不会执行使用另一个线程的 sigsetjmp() 结果的 siglongjmp()

此外,sigsetjmp()siglongjmp() 可以恢复和保存信号掩码,而 setjmp(3C)longjmp(3C) 不会执行这些操作。

使用信号处理程序时,请使用 sigsetjmp()siglongjmp()

完成语义通常用于处理异常。需要特别指出的是,Sun AdaTM 编程语言就使用此模型。


注 –

请记住,不得sigwait(2) 与同步信号一同使用。


信号处理程序和异步信号安全

与线程安全类似的概念就是异步信号安全。异步信号安全操作可保证不会干扰正被中断的操作。

当信号处理程序操作干扰正被中断的操作时,就会引发异步信号安全问题。

例如,假设程序正在调用 printf(3S),且其调用程序调用 printf() 时出现了信号。在这种情况下,两个 printf() 语句的输出彼此关联。要避免关联输出,当 printf() 可能被信号中断时,处理程序不应直接调用 printf()

无法使用同步元语来解决此问题。 在信号处理程序与正被同步的操作之间执行的任何同步尝试都将立即产生死锁现象。

假设 printf() 借助互斥来保护自身。现在,假设调用 printf() 进而通过互斥锁持有锁定的线程被信号中断。

如果处理程序调用 printf(),则通过互斥锁持有锁定的线程将尝试再次利用互斥锁。尝试利用互斥锁将导致瞬间死锁。

为避免处理程序与操作之间出现干扰,请确保这种情况永远不会发生。或许您可以在关键时候屏蔽信号,或从内部信号处理程序中仅调用异步信号安全操作。

表 5–2 中列出了 POSIX 可确保异步信号安全的仅有例程。任何信号处理程序都可以安全地调用这些函数之一。

表 5–2 异步信号安全函数

_exit()

fstat()

read()

sysconf()

access()

getegid()

rename()

tcdrain()

alarm()

geteuid()

rmdir()

tcflow()

cfgetispeed()

getgid()

setgid()

tcflush()

cfgetospeed()

getgroups()

setpgid()

tcgetattr()

cfsetispeed()

getpgrp()

setsid()

tcgetpgrp()

cfsetospeed()

getpid()

setuid()

tcsendbreak()

chdir()

getppid()

sigaction()

tcsetattr()

chmod()

getuid()

sigaddset()

tcsetpgrp()

chown()

kill()

sigdelset()

time()

close()

link()

sigemptyset()

times()

creat()

lseek()

sigfillset()

umask()

dup2()

mkdir()

sigismember()

uname()

dup()

mkfifo()

sigpending()

unlink()

execle()

open()

sigprocmask()

utime()

execve()

pathconf()

sigsuspend()

wait()

fcntl()

pause()

sleep()

waitpid()

fork()

pipe()

stat()

write()

中断对条件变量的等待

将捕获到的已取消屏蔽的信号传送到等待条件变量的线程时,线程将从虚假唤醒的信号处理程序中返回。 虚假唤醒是指不是由其他线程中的条件信号调用导致的唤醒。在这种情况下,Solaris 线程接口 cond_wait()cond_timedwait() 将返回 EINTR,而 POSIX 线程接口 pthread_cond_wait()pthread_cond_timedwait() 将返回 0。在所有情况下,从条件等待返回之前都将重新获取关联的互斥锁定。

重新获取关联的互斥锁定并不暗示线程在执行信号处理程序的同时,互斥处于锁定状态。未定义信号处理程序中的互斥状态。

由于在 Solaris 9 发行版之前的 Solaris 软件发行版中实现了 libthread,因而保证了在处于信号处理程序中时保留了互斥。依赖此原有行为的应用程序需要修改 Solaris 9 发行版以及后续发行版。

示例 5–4 将对处理程序清除加以说明。


示例 5–4 条件变量和中断的等待

int sig_catcher() {

    sigset_t set;

    void hdlr();



    mutex_lock(&mut);



    sigemptyset(&set);

    sigaddset(&set, SIGINT);

    sigsetmask(SIG_UNBLOCK, &set, 0);



    if (cond_wait(&cond, &mut) == EINTR) {

        /* signal occurred and lock is held */

        cleanup();

        mutex_unlock(&mut);

        return(0);

    }

    normal_processing();

    mutex_unlock(&mut);

    return(1);

}



void hdlr() {

    /* state of the lock is undefined */

    ...

}

假设 SIGINT 信号在进入 sig_catcher() 时在所有线程中受到阻塞。此外,还假设已通过调用 sigaction(2) 建立了 hdlr()(作为 SIGINT 信号的处理程序)。如果在线程处于 cond_wait() 中时将捕获到的已取消屏蔽的 SIGINT 信号实例传送到该线程,该线程将调用 hdlr()。然后,线程将返回到 cond_wait() 函数(如有必要,将在此处重新获取互斥锁定),并从 cond_wait() 返回 EINTR

是否已针对 sigaction()SA_RESTART 指定为标志在此处无影响。cond_wait(3C) 不是系统调用,也不会自动重新启动。如果在 cond_wait() 中阻塞线程时出现捕获的信号,则调用将始终返回 EINTR