分类: LINUX
2008-12-09 13:42:19
POSIX线程机制定义了多种数据类型,这些数据类型对应用程序来说其内部结构是不透明的。就是说直接访问它们的数据对象是无意义的,而应该使用pthreads(7)库定义的方法去进行访问。各种数据类型对象方法的动作包括初始化、销毁、读取、更改等。
初始化和去初始化
在使用线程属性对象之前,应该使用对其进行初始化,如果已经使用结束,应销毁此对象以释放进程空间:
#include
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
分离属性
下面的函数用于读取和设置线程的分离属性:
#include
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
线程的分离属性只有两个选项。为PTHREAD_CREATED_JOINABLE时,线程退出时需要另外一个线程使用pthread_join(3)来回收线程资源。为PTHREAD_CREATED_DETACHED时,线程退出时不需要join。
栈属性
线程使用栈地址和栈大小这两个属性来描述线程使用的栈。读/写线程栈属性的函数为
#include
int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);
int pthread_attr_setstack(const pthread_attr_t *attr, void *stackaddr, size_t *stacksize);
系统将参数中的线程栈地址定义为线程栈的最低地址空间。还可以使用以下函数单独设置线程的栈大小属性:
#include
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
栈警戒区
栈警戒区在线程栈空间末尾之后,在线程空间溢出到警戒区时,线程将收到信号通知(一般是SIGSEGV)。栈警戒区的默认大小为PAGESIZE,如果自行定义了任何线程属性,但不修改线程的警戒区属性,则警戒区大小将被置零。
设置和修改栈警戒区属性的函数为:
#include
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
未定义在pthread_attr_t中的线程属性
主要包括取消选项以及并发度选项。并发度描述了应用程序线程可映射的内核线程数,即对于用户程序,最多可以有多少个线程可以“同时”执行系统调用,在操作系统实现为一个内核线程只映射为一个用户进程时,并发度的提高有助于改善程序性能。
#include
int pthread_getconcurrency(void);
int pthread_setconcurrency(int level);
在未设置并发度时,pthread_getconcurrency将返回0。pthread_setconcurrency的设置值不一定会被内核接受,而当参数为0时,将取消之前一次pthread_setconcurrency的设置,而让内核自行调度。
互斥量的属性
互斥量属性对象的初始化和回收方法为
#include
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
互斥量属性比较重要的是进程共享属性和类型属性。
共享属性包括PTHREAD_PROCESS_SHARED和PTHREAD_PROCESS_PRIVATE这两个互斥的值,采用前者时该互斥量可供多个进程共享用于同步,后者则使互斥量只供本进程内的线程使用。
获得和修改当前互斥量共享属性的函数为
int pthread_mutexattr_getpshared(const pthread_mutexattr *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
类型属性包括:
PTHREAD_MUTEX_NORMAL 正常(不检查错误,非递归锁)
PTHREAD_MUTEX_ERRORCHECK 检查对互斥量的错误使用
PTHREAD_MUTEX_RECURSIVE 使互斥量成为递归锁
PTHREAD_MUTEX_DEFAULT 通常是其它三种类型之一的别名
递归锁的意思是同一线程可以重复占有该互斥锁,只有在加锁次数和解锁次数相同的情况下才会释放该锁。递归锁的使用需要一定的技巧,APUE建议在没有其它替代方案才使用。例如一个递归函数内有需要保护的临界区时,就应该使用递归锁。
读写锁的属性
唯一的属性为进程共享属性,同互斥量的的进程共享属性。
#include
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destory(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
条件变量的属性
同样也只有进程共享属性。
#include
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destory(pthread_condattr_t *attr);
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
称一个函数为线程安全的(),如果此函数被多个线程同时调用时行为不会被彼此干扰而能保证执行正确。如果一个函数可重入,那么它是线程安全的,但使用了非可重入函数(例如malloc(3))的线程也能通过互斥等同步机制实现线程安全。所以不能从线程安全函数是可重入函数的结果,特别是线程安全并不能保证异步信号安全。
POSIX还提供了以线程安全的方式访问FILE对象的方法,即针对FILE对象的锁同步机制:
#include
int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
线程私有数据使用线程键关联一个地址,使该地址可以被当前线程关联,从而达到因需要而使单个线程独享某些进程地址空间的目的。
应注意的一点是,由于各线程访问同一个进程已在堆或者栈上分配的内存单元是不受限制的,所以“私有数据”(实际上为“”即为MFC中的)实际上并不能阻止其它线程访问所指定的地址。它只是提供了一种建议性的统一机制,让程序遵循这种以键关联私有数据的规则来访问各自的地址空间,以避免读写到不必要的地址。
创建一个键:
#include
int pthread_key_create(pthread_key_t *key, void(*destructor)(void *));
所创建的键放在key指向的地址,故应注意调用此函数之前,key已经(以静态或者动态的方式)分配了内存空间。同时可以定义此键的析构函数,此析构函数将在线程正常退出时被调用,通常可以用来释放以动态方式分配的key的空间,以避免内存泄漏。
将键与线程私有数据的地址关联起来:
#include
int pthread_setspecific(pthread_key_t key, const void *value);
此后线程就可以通过该键来访问私有数据:
#include
void *pthread_getspecific(pthread_key_t key);
删除当前线程与某个键所关联的数据的私有性(解除关联):
#include
int pthread_key_delete(pthread_key_t key);
应注意:此函数只是解除关联,并不会调用键的析构函数,就是说该键的内存单元不会因此而被释放。
使某函数在线程内只被调用一次:
#include
int pthread_once(pthread_once_t *initflag, void(*initfn)(void));
注意:initflag不能是局部变量,且应已经使用类似下面的语句进行初始化:
pthread_once_t initflag = PTHREAD_ONCE_INIT;
pthread_once可以通过原子的测试和修改initflag以保证该初始化函数只被调用一次,初始化函数通常就可以用于线程初始化时进行创建私有键之类的工作。可以简单的编程验证,重新初始化initflag后,可以重新调用初始化函数initfn。
* 对于书中的程序清单12-5(英文版的Figure 12.13),在FreeBSD下编译运行将会出错,因为FreeBSD的malloc(3)实现调用了getenv(3),而书中程序的getenv实现又调用了malloc(3),从而产生双关递归而导致死循环。自己实现库函数时也要注意防止出现这样的问题。
包括取消状态和取消类型。
#include
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
这两个函数分别用于设置取消状态和取消类型,同时原子地将原来的值备份到第二个参数中。
取消状态用于允许或禁止响应pthread_cancel(3)的请求。包括PTHREAD_CANCEL_ENABLE和 PTHREAD_CANCEL_DISABLE这两个选项。
取消类型用于指定响应pthread_cancel(3)后应该如何终止线程的运行。选项包括PTHREAD_CANCEL_DEFERED和 PTHREAD_CANCEL_ASYNCHRONOUS。前者将在运行到指定的取消点时终止线程,后者将立即终止线程。POSIX指定了哪些函数应该具有线程的取消点。也可以用下面的函数自行设置取消点:
#include
int pthread_testcancel(void);
不同的线程,信号屏蔽字是独立的,但对信号的处理方式是共享的,而且新线程的未决信号集为空。信号是直接发到单个线程的,硬件故障或者计时器超时的信号会直接发到引起这类信号产生的线程中,其它例如SIGUSR1之类的信号则会随机发送到任意一个未屏蔽此信号的线程。
在线程中屏蔽信号的函数为
#include
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
它的用法类似sigprocmask(2)。
在多线程编程中处理信号典型的措施是:指定一个专门的线程来处理指定的信号,而其它线程则屏蔽这个信号。专门处理信号的线程可以使用sigwait(3)函数来实现,该函数等待指定信号集中的信号被阻塞:
#include
sigwait(const sigset_t *restrict set, int *restrict signop);
注意此函数与sigsuspend(2)的区别。sigwait(3)将阻塞到指定的信号集set中的信号在进程的未决信号集中出现,此时将原子的在进程未决信号集中清掉该信号对并解除对其的阻塞(但实际上并不改变进程的信号屏蔽字,只是让这个信号能被递送),同时在signop中记录清掉的信号数量,然后返回。而sigsuspend(2)是阻塞到不在指定的信号集中的信号出现。所以在调用sigwait之前,应该先阻塞要等待的信号。这样就可以使得异步信号同步在sigwait(3)上。其它线程由于已经屏蔽了这个信号,所以不用考虑异步信号安全的问题。
可以用下面的函数对指定的线程发送信号
#include
int pthread_kill(pthread_t thread, int signo);
可以通过发送0信号以测试指定的线程是否存在。另外应注意默认为终止进程的信号会杀死整个进程而不是仅仅杀死线程。还应注意的是,alarm(2)设置的闹钟定时器是进程的全局资源,在单独线程中使用的时候应注意是否会带来副作用。
虽然多线程实现了在一个进程内并行的进行多任务处理,但仍然需要使用fork(2)创建子进程来执行一个程序。
调用fork(2)创建的子进程实际上是父进程中调用fork的线程的一个副本。但由于子进程继承父进程的所有上下文,包括互斥量等锁的状态,而调用了fork的线程未必占有这些锁资源,所以如果子进程不立即exec的话,应解除调用fork的线程所占有之外的其它锁。
可以使用pthread_atfork(3)注册fork处理程序进行这些工作来解决此问题。
#include
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
该函数用于注册三个fork处理函数。其中prepare指向的函数在子进程被生成之前调用。在子进程生成后,fork(2)返回前,parent指向的函数将在父进程中被调用,而child指向的函数将在子进程中被调用。
如果多次调用pthread_atfork注册fork处理函数,perpare按和注册顺序相反的顺序被调用,而parent和child按注册的顺序被调用。
这样,就可以在parent函数中先对所有的锁属性设置为递归锁,然后以阻塞方式对所有的锁进行加锁,并在parent和child中对所有的锁解锁,再恢复锁的属性。
最后还应注意,在子进程中使用原来的条件变量是不安全的,应把它们都销毁掉。
pread(2)和pwrite(2)可以使多线程程序安全的以原子操作方式定位打开的文件并进行读写。