Duangw

线程编程知识

索引:

  1. 单线程复制模型
  2. 安全复制pthread_atfork
  3. 多线程复制模型
  4. 线程环境的exec()和exit()
  5. 非局部跳转语句setjmp()和longjmp()
  6. 信号的扩展
  7. 线程安全函数
  8. 接口的多线程安全性级别
  9. 对应于不安全接口的可重入函数
  10. 多线程程序设计时常见问题

1.单线程复制模型

概念

POSIX线程接口的fork()是单线程复制模型,即函数创建一个新的进程,复制父进程的地址空间,但在子进程中只复制父进程中调用复制fork()函数的线程。

当需要在子进程中立即调用exec()函数时,使用这种模型就不需要复制所有的线程,这样可以减少资源浪费。

在子进程中,在fork()函数调用和exec()函数调用之间,不要调用任何库函数,因为在子进程调用一个库函数时,可能使用了父进程在调用fork()函数前已锁定的锁而导致死锁。所以在子进程执行exec()函数之前只能执行异步信号安全(Async-Signal-Safe)的操作。

防止死锁

为了防止死锁,要保证复制进程时没有上述情况。最简单的方法就是由调用fork函数的线程预先锁定所有子进程中可能使用的锁。但显然对于在printf()函数中使用的锁,程序是无法锁定的。这时必须保证在调用fork()函数时,没有printf()函数被调用。

为了管理好设计的库中的锁,须注意以下几点:

  1. 标志出库中使用的所有锁;
  2. 确定出锁的锁定顺序,如果锁定次序不能固定,必须小心处理锁定过程,避免死锁;
  3. 在复制进程前锁定库中所有的锁。

在POSIX线程中,可以在库的init()函数中调用pthread_atfork(f1,f2,f3)函数,这种情况下,使用库的程序员在复制单线程时不需要逐个锁定库中的锁。f1()、f2()、f3()函数的作用是这样的:

详情参见pthread_atfork()的说明。

虚复制vfork

标准的vfork()函数对多线程来说是不安全的,它一样只复制当前线程到子进程,但vfork()函数不为子进程复制内存空间。

使用vfork()函数时,务必小心的是在子进程调用exec()函数前,子进程和父进程拥有同一个地址空间,只有在子进程终止或调用了exec()函数后,父进程才能独占自己的地址空间。所以在子进程中千万不要改变父进程的状态。

全局状态

在调用了fork()函数后必须小心使用全局状态。

例如,当一个线程正在顺序的读文件时,进程中的另一个线程成功的调用了fork函数,这时每个进程都会有一个线程在读文件。因为在fork函数后文件的当前位置的指针也是共享的,所以就会出现父进程读到一些数据而子进程读到却是另外一些数据。这导致了顺序读访问读出的数据中有遗漏。

 

2.安全复制pthread_atfork

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void(*parent)(void), \
	void(*child)(void));
返回值:函数成功返回0;任何其他返回值都表示错误

函数指定了在fork()函数前后被当前线程调用的函数。其中:

参数prepare指定的函数在fork()被调用之前被调用、参数parent指定的函数在fork()函数从父进程中返回后被调用、参数child指定的函数在fork()函数从子进程中返回后被调用。

这3个参数中的任何一个都可以被指定为NULL。若要连续多次调用pthread_atfork(),调用顺序是十分重要的。

pthread_atfork()函数指定的3个函数一般完成下列功能:prepare函数锁定库中所有的锁,parent和child函数释放这些锁。这就保证了在fork函数之前,当前线程(调用fork()函数的线程)占用库中使用的所有锁,而在fork函数之后父进程和子进程将分别释放各自被锁定的锁,从而在子进程中不会发生死锁。

 

3.多线程复制模型

POSIX线程标准不支持这种复制。

 

4.线程环境的exec()和exit()

exec()函数和exit()函数所完成的功能和它们在单线程程序中完成的功能是一样的。在多线程程序中调用这两个函数将会结束所有线程。这两个函数在释放完所有程序运行所用的资源(包括所有线程)之前将会阻塞。

当exec()函数重建进程之后,它将创建一个轻进程(LWP)。进程的启动代码将创建初始线程。如果初始线程返回,它将调用exit()函数最终终止进程。

如果在一个多线程的进程中调用exec()函数将会终止所有的线程,并装载执行新的可执行代码。这两个函数将不会调用任何析构函数。

 

5.非局部跳转语句setjmp()和longjmp()

setjmp()函数和longjmp()函数的有效范围被限制在一个线程中,大部分情况下在线程中转移已经够用了。

这限制了当一个信号发生时,接受到信号的线程在处理信号时,只能利用longjmp()函数跳转到同一个线程中调用了setjmp()函数的地方。

sigsetjmp()和siglongjmp()会保存和恢复线程的信号掩码,但setjmp()和longjmp()函数则不会。当你使用信号处理函数时,最好使用sigsetjmp()和siglongjmp()函数。

 

6.信号的扩展

(1).新的语义

传统UNIX系统中的信号机制自然地扩展到了多线程的UNIX系统中。这种扩展的信号机制的主要特点是信号是发往进程的,而信号掩码则是针对线程的。

进程范围的信号响应可用通过传统的信号函数signal()、sigaction()等等来实现。

当一个信号处理程序被设置为SIG_DFL或SIG_IGN时,收到信号后的处理将针对整个进程,影响所有进程中的线程。对于那些没有处理函数的信号,由于收到信号后的处理将针对整个进程,所以哪个线程收到了信号是不重要的。

每一个线程都有它们自己的信号掩码。这使线程可以不响应某些信号。所有一个进程中的线程都共享由sigaction()和它的变体函数所设置的一套信号处理函数。

一个进程中的线程不能针对另一个进程中的某个指定线程发送信号。由kill()函数或sigsend()函数发给一个进程的信号可以被进程中的任何一个线程捕获并响应。

非绑定线程不能使用非缺省的信号堆栈,一个绑定线程可以使用非缺省的信号堆栈,这是由于非缺省的信号堆栈是和执行资源相关联的。

一个应用程序可以在基于进程的信号处理的基础上建立针对线程的信号处理。方式之一就是在信号处理函数中取当前线程的标识符作为索引来查找处理函数表(这个表记录了各个线程各自的处理信号函数)。

对多线程程序来说,特别重要的情况是信号对pthread_cond_wait()函数的影响。这个函数往往在其他线程调用了pthread_cond_signal()或pthread_cond_broadcast()函数后返回,但是如果等待pthread_cond_wait()函数返回的线程收到的一个传统的UNIX信号,这个线程将从pthread_cond_wait()函数中返回,并返回错误代码EINTR。

(2).同步信号

信号可以被分成两类:陷阱和异常(同步产生的信号)、中断(异步产生的信号)。

陷阱(如SIGILL,SIGFPE,SIGSEGV)是由于线程自身的行为产生的,如除0。一个陷阱只能被产生它的线程处理。可以同时有几个线程产生和处理同一种陷阱。

对于同步信号来说,将可将其看作是针对线程的,因为处理信号的线程就是产生信号的线程。

然而,如果线程没有指定信号处理函数来响应同步信号,信号将会被收到同步信号的其他线程所响应。

由于这样的同步信号往往意味着对于整个进程来说有严重的错误,不仅仅是对线程而言,所以这时退出进程往往是明智的选择。

(3).异步信号

中断(例如SIGINT和SIGIO)都是异步产生的,往往都产生于进程之外。这类信号有可能是由其他线程产生的,或是进程外的动作产生的。

一个中断可以被信号掩码设为允许状态的任何线程所响应。如果多于一个线程可以响应该信号,那么系统将挑选一个线程来响应该信号。

当多个相同的信号发生时,有可能每一个信号都由不同的线程来处理。只要可以处理该信号的线程数足够。如果所有的线程都用信号掩码禁止了对该信号的响应,那么信号将会被挂起直到有线程解开相应的掩码。

(4).持续语义

持续语义是传统的处理信号的方式。也就是说,当信号处理返回时,控制将返回到被信号中断的程序那里继续运行。

(5).函数pthread_sigsetmask

设置线程的信号掩码。

当创建一个新线程时,它的初始化掩码是从它的创建者那里继承的。

在多线程程序中调用sigprocmask()函数等价于调用pthread_sigsetmask()函数。

(6).函数pthread_kill

向某线程发一个信号。这和向一个进程发信号是不同的,当一个信号被发往进程时,信号可以被进程中的任何一个线程所捕获。而pthread_kill()函数发的信号只能被指定的线程所捕获。

只能用pthread_kill()函数向本进程内的线程发信号。

需要注意的是,当收到信号后处理函数(或SIG_DFL,SIG_IGN)进行的操作仍是全局的,针对进程的。例如:向一个线程发信号SIGXXX,进程对信号SIGXXX的处理是退出进程,则当目标线程收到信号后,整个进程将退出。

(7).函数sigwait

#include <signal.h>
int sigwait(const sigset_t *set, int *sig);

当信号到达时,sigwait()清除挂起的信号,并把传来的信号值赋给参数sig。

sigwait()函数使当前线程保持等待直到某些信号发生。当线程在等待这些信号时,相应的信号掩码将被打开。但sigwait()函数返回后,相应的信号掩码被重新恢复成原来的状态。

所有指定的信号必须被所有的线程屏蔽,包括当前线程:否则sigwait()函数无法正常工作。注意sigwait()函数会自行打开指定信号的信号掩码。

可以有许多线程同时调用sigwait()函数,但当一个信号发生时,只会有一个线程收到信号并返回。

用sigwait()函数可以让一个线程专门处理异步信号,使其他线程不用考虑异步信号的处理。可以创建一个线程一直等待异步信号。而在进程的其他线程中则用信号掩码阻塞异步信号,这样程序在处理信号的问题上将会很安全。

注意:sigwait()函数不能用在同步信号上。

(8).函数sigtimedwait

sigtimedwait()和sigwait()函数大体相同,除了一点:在等待了一段时间以后,如果等不到信号sigtimedwait函数将会返回错误代码。

(9).异步信号安全性

和线程安全性相同的一个概念是异步信号安全性。异步信号安全的操作可以保证不会影响被信号中断的操作。

异步信号安全问题发生在信号处理函数有可能对被信号中断的操作产生影响的时候。这个问题无法用同步原语解决,因为任何试图在信号处理函数和操作之间进行的同步都会立即产生死锁。

为了防止被中断操作和信号处理程序之间的冲突,必须保证相应的操作不会被中断(可以通过在关键时刻屏蔽信号)或在信号处理函数中只调用异步信号安全函数。

由于设置线程的信号掩码(屏蔽信号)是一种耗费很少的用户层操作。你可以简单的通过信号屏蔽来实现异步信号安全。

POSIX异步信号安全函数如下,任何信号处理函数久可以安全的调用这些函数:

_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()

(10).中断对条件变量的等待

在POSIX线程中,pthread_cond_wait()函数遇到信号时会返回,但不会返回错误代码,这种情况下,函数就像条件变量被唤醒一样返回0。

 

7.线程安全函数

线程安全的函数是指:当这个函数同时被多个线程调用时,函数仍然能确保其在逻辑上的正确性。实际上,可以将线程安全性分为三个级别:

  1. 不安全的;
  2. 线程安全的-串行化的(Serializable);
  3. 线程安全的-多线程安全的(MT-safe);

一个不安全的函数可以通过在调用函数时使用互斥锁来保证函数的线程安全性。

串行化的函数用互斥锁来防止函数被几个线程并发调用。

多线程安全的函数是指:函数是线程安全的(不存在数据竞争)同时不会对函数的性能有消极的影响(与串行化比较)。

 

8.接口的多线程安全性级别

安全的(Safe) 函数可以在多线程程序中调用
大致安全的(Safe with exceptions) 除了一些特殊情况
不安全的(Unsafe) 在多线程程序中使用这个函数是不安全的,除非由程序保证一次只有一个线程调用相应函数库中的函数
多线程安全的(MT-Safe) 这个函数对于多线程程序来说是安全的,而且它支持一定程度的并发执行
大致多线程安全的(MT-Safe withexceptions) 除了一些特殊情况
异步信号安全的(Asyn-Signal-Safe) 可以在信号处理函数中调用这个函数,一个在异步信号安全函数中运行的线程不会由于被信号中断后调用同一个函数而死锁
单线程复制安全的(Fork1-Safe) 这样的函数或函数库在有线程调用fork()函数时,会解锁它们已锁定的锁

 

9.对应于不安全接口的可重入函数

对于不安全接口中的大多数函数来说,都有一个多线程安全的函数与之对应。这个多线程安全函数的函数名往往是原来的非安全函数的函数名加上一个“_r”的后缀。

 

10.多线程程序设计时常见问题

把一个指向线程堆栈中变量的指针做为线程的输入参数传递到子线程中。

不用同步机制保护对全局变量的访问。

在两个线程中按不同的次序访问两个互斥锁造成的死锁。

锁定一个已经被本线程锁定了的互斥锁。

同步保护中存在有隐藏的缺口,往往是由于在保护区中调用的函数将互斥锁释放后再重新锁定。结果是从程序上看似乎提供了同步保护,其实却没有保护好。

在线程运行时发生的信号未被合理处理,最好是用一个线程调用sigwait函数来专门处理信号。

使用setjmp()和longjmp(),但是在调用longjmp函数后没有释放互斥锁。

从pthread_cond_wait()或pthread_cond_timedwait()函数中返回时,没有重新判断一下条件是否满足。

忘了缺省的线程都是PTHREAD_CREATE_JOINABLE的,而且必须用pthread_join()函数来归还资源。注意pthread_exit()函数并不释放线程的资源。

使用大量嵌套或递归函数或使用大的局部数组可能会造成堆栈溢出,因为多线程程序的堆栈大小更有限。

指定的堆栈过小,或使用非默认堆栈。