UNIX-线程(下)线程控制

UNIX编程第12章

12.2 线程限制

UNIX中线程操作相关的限制:

限制名称----描述----name参数

PTHREAD_DESTRUCTOR_ITERATORS----线程退出时操作系统实现试图销毁线程特定数据的最大次数----_SC_THREAD_DESTRUCTOR_ITERATIONS

PTHREAD_KEYS_MAX----进程可以创建的键的最大数目----_SC_THREAD_KEYS_MAX

PTHREAD_STACK_MIN----一个线程的栈可用的最小字节数----_SC_THREAD_STACK_MIN

PTHREAD_THREADS_MAX----进程可以创建的最大线程数----_SC_THREAD_THREADS_MAX

其中name参数用于sysconf函数查询

12.3 线程属性

pthread接口中对线程和同步对象进行属性管理的模式:

-每个对象与它自己类型的属性对象进行关联(线程与线程属性关联,互斥量与互斥量属性关联,等等)。一个属性对象可以代表多个属性。提供相应的函数来管理这些属性对象。

-有一个初始化函数,把属性设置为默认值。

-还有一个销毁属性对象的函数。如果初始化函数分配了与属性对象关联的资源,销毁函数负责释放这些资源。

-每个属性都有一个从属性对象中获取属性值的函数。由于函数成功时会返回0,失败时会返回错误编号,所以可以通过把属性值存储在函数的某一个参数指定的内存单元中,把属性值返回给调用者。

-每个属性都有一个设置属性值的函数。在这种情况下,属性值作为参数按值传递。

可以使用pthread_attr_init函数初始化pthread_attr_t结构。在调用pthread_attr_init以后,pthread_attr_t结构所包含的就是操作系统实现支持的所有线程属性的默认值。

#include<pthread.h>

int pthread_attr_init( pthread_attr_t *attr);

int pthread_attr_destroy( pthread_attr_t *attr);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

pthread_attr_destroy函数反初始化,如果pthread_attr_init的实现对属性对象的内存空间是动态分配的,pthread_attr_destroy就会释放该内存空间。除此之外,pthread_attr_destroy还会用无效的值初始化属性对象,因此,如果该属性对象被误用,将会导致pthread_create函数返回错误码。

线程的属性:

名称---描述

detachstate---线程的分离状态

guardsize---线程栈末尾的警戒缓冲区大小(字节数)

stackaddr---线程栈的最低地址

stacksize---线程栈的最小长度(字节数)

如果对现有的某个线程的终止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时收回它所占用的资源。

如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始就处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的终止状态。

#include<pthread.h>

int pthread_attr_getdetachstate( const pthread_attr_t *restrict attr, int *detachstate);

int pthread_attr_setdetachstate( pthread_attr_t *attr, int *detachstate);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

可以调用pthread_attr_getdetachstate函数获取当前的detachstate线程属性。第二个参数所指向的整数要么设置成PTHREAD_CREATE_DETACHED,要么设置成PTHREAD_CREATE_JOINABLE,具体要取决于给定pthread_attr_t结构中的属性值。

可以在编译阶段使用_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE符号来检查系统是否支持每一个线程属性。

可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。

#include<pthread.h>

int pthread_attr_getstack( const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);

int pthread_attr_setstack( pthread_attr_t *attr, void *stackaddr, size_t stacksize);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置。对于一个给定的处理器结构来说,如果栈是从高地址向低地址方向增长的,那么stackaddr线程属性将是栈的结尾位置,而不是开始位置。

应用程序也可以通过pthread_attr_getstacksize和pthread_attr_setstacksize函数读取或设置线程属性stacksize。

#include<pthread.h>

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);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN。

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认值是由具体实现来定义的,但常用值是系统页大小。可以把guardsize线程属性设置为0,不允许属性的这种特征行为发生:在这种情况下,不会提供警戒缓冲区。同样,如果修改了线程属性stackaddr,系统就认为我们将自己管理栈,进而使栈警戒缓冲区机制无效,这等同于把guardsize线程属性设置为0.

#include<pthread.h>

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);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

如果guardsize线程属性被修改了,操作系统可能会把它取为页大小的整数倍。如果线程的栈指针溢出到警戒区域,应用程序就可能通过信号接收到出错信息。

12.4 同步属性

12.4.1 互斥量属性

互斥量属性是用pthread_mutexattr_t结构表示的。对于非默认属性,可以用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destroy来反初始化。

#include<pthread.h>

int pthread_mutexattr_init( pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy( pthread_mutexattr_t *attr);

  两个函数的返回值:若成功,返回0;否则返回错误编号。

pthread_mutexattr_init函数将用默认的互斥量属性初始化pthread_mutexattr_t结构。值得注意的3个属性是:进程共享属性、健壮属性以及类型属性。

在进程中,多个线程可以访问同一个同步对象。在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE。

存在这样的机制:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

可以使用pthread_mutexattr_getpshared函数查询pthread_mutexattr_t结构,得到它的进程共享属性,是用pthread_mutexattr_setpshared函数修改进程共享属性。

#include<pthread.h>

int pthread_mutexattr_getpshared( const pthread_mutexattr_t *restrict attr, int *restrict pshared);

int pthread_mutexattr_setpshared( pthread_mutexattr_t *attr, int pshared);

  两个函数的返回值:若成功返回0,否则返回错误编号。

互斥量健壮属性与在多个进程间共享的互斥量有关。这意味着,当持有互斥量的进程终止时,需要解决互斥量状态恢复的问题。这种情况下,互斥量处于锁定状态,恢复起来很困难。其它阻塞在这个锁的进程将会一直阻塞下去。

可以使用pthread_mutexattr_getrobust函数获取健壮的互斥量属性的值。可以调用pthread_mutexattr_setrobust函数设置健壮的互斥量属性的值。

#include<pthread.h>

int pthread_mutexattr_getrobust( const pthread_mutexattr_t *restrict attr, int *restrict robust);

int pthread_mutexattr_setrobust( pthread_mutexattr_t *attr, int robust);

  两个函数的返回值:若成功,返回0;否则,返回错误编号;

健壮属性取值有两种可能的情况。默认值是PTHREAD_MUTEX_STALLED,这意味着持有互斥量的进程终止时不需要采取特别的动作。这种情况下,使用互斥量后的行为是未定义的,等待该互斥量解锁的应用程序会被有效地“拖住”。另一个取值是PTHREAD_MUTEX_ROBUST,这个值将导致线程调用pthread_mutex_lock获取锁,而该锁被另一个进程持有,但它终止时并没有对该锁进行解锁,此时线程会阻塞,从pthread_mutex_lock返回的值为EOWNERDEAD而不是0。应用程序可以通过这个特殊的返回值获知,若有可能(要保护状态的细节以及如何进行恢复会因不同的应用程序而异),不管它们保护的互斥量状态如何,都需要进行恢复。

使用健壮的互斥量改变了我们使用pthread_mutex_lock的方式,因为现在必须检查3个返回值而不是之前的两个:不需要恢复的成功、需要恢复得成功以及失败。不使用健壮属性时,也可以检查成功或者失败。

如果应用状态无法恢复,在线程对互斥量解锁以后,该互斥量将处于永久不可用状态。为了避免这样的问题,线程可以调用pthread_mutex_consistent函数,指明与该互斥量相关的状态在互斥量解锁之前是一致的。

#include<pthread.h>

int pthread_mutex_consistent( pthread_mutex_t *mutex);

  返回值:若成功,返回0;否则返回错误编号;

如果线程没有先调用pthread_mutex_consistent就对互斥量进行了解锁,那么其它试图获取该互斥量的阻塞线程就会得到错误码ENOTERCONVERABLE。如果发生这种情况,互斥量将不再可用。线程通过提前调用pthread_mutex_consistent,能让互斥量正常工作,这样它就可以持续被使用。

类型互斥量属性的锁定特性。POSIX.1定义了4种类型:

-PTHREAD_MUTEX_NORMAL  一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。

-PTHREAD_MUTEX_ERRORCHECK  此互斥量类型提供错误检查。

-PTHREAD_MUTEX_RECURSIVE  此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。

-PTHREAD_MUTEX_DEFAULT  此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其它互斥量类型中的一种。

可以用pthread_mutexattr_gettype函数得到互斥量类型属性,用pthread_mutexattr_settype函数修改互斥量类型属性。

#include<pthread.h>

int pthread_mutexattr_gettype( const pthread_mutexattr_t *restrict attr, int *restrict type);

int pthread_mutexattr_settype( pthread_mutexattr_t *attr, int type);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

线程(上)中说过,在线程使用条件变量时,需要互斥量进行加锁解锁保护。pthread_cond_wait和pthread_cond_timedwait函数释放与条件相关的互斥量。此时不适合使用递归互斥量。

12.4.2 读写锁属性

读写锁与互斥量类似,也有属性。可以用pthread_rwlockattr_init初始化pthread_rwlockattr_t结构,用pthread_rwlockattr_destroy反初始化该结构。

#include<pthread.h>

int pthread_rwlockattr_init( pthread_rwlockattr_t *attr);

int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

  两个函数的返回值:若成功,返回0;否则,返回错误编号;

读写锁支持的唯一属性是进程共享属性。

#include<pthread.h>

int pthread_rwlockattr_getpshared( const pthread_rwlockattr_t *restrict attr, int *restrict pshared);

int pthread_rwlockattr_setpshared( pthread_rwlockattr_t *attr, int pshared);

  两个函数的返回值:若成功,返回0;否则,返回错误编号;

12.4.3 条件变量属性

条件变量两个属性:进程共享和时钟属性。

#include<pthread.h>

int pthread_condattr_init( pthread_condattr_t *attr);

int pthread_condattr_destroy( pthread_condattr_t *attr);

  两个函数的返回值:若成功,返回0;否则,返回错误编号;

与其它的同步属性一样,条件变量支持进程共享属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用。

#include<pthread.h>

int pthread_condattr_getpshared( const pthread_condattr_t *restrict attr, int *restrict pshared);

int pthread_condattr_setpshared( pthread_condattr_t *attr, int pshared);

  两个函数的返回值:若成功,返回0;否则返回错误码;

时钟属性控制计算pthread_cond_timedwait函数的超时参数(tsptr)时采用的是哪个时钟。可以使用pthread_condattr_getclock函数获取可被用于pthread_cond_timedwait函数的时钟ID,在使用pthread_cond_timedwait函数前需要用pthread_condattr_t对象对条件变量进行初始化。可以用pthread_condattr_setlock函数对时钟ID进行修改。

#include<pthread.h>

int pthread_condattr_getlock( const pthread_condattr_t *restrict attr, clockid_t *restrict clock_id);

int pthread_condattr_setclock( pthread_condattr_t *attr, clockid_t clock_id);

  两个函数的返回值:若成功,返回0;否则,返回错误码;

时钟ID clockid_t可取类型:

标识符---选项---说明

CLOCK_REALTIME----         ----实时系统时间

CLOCK_MONOTONIC----_POSIX_MONOTONIC_CLOCK----不带负跳数的实时系统时间

CLOCK_PROCESS_CPUTIME_ID----_POSIX_CPUTIME----调用进程的CPU时间

CLOCK_THREAD_CPUTIME_ID----_POSIX_THREAD_CPUTIME----调用线程的CPU时间

12.4.4 屏障属性

屏障也有属性。可以使用pthread_barrierattr_init函数对屏障属性对象进行初始化,用pthread_barrierattr_destroy函数对屏障属性对象进行反初始化。

#include<pthread.h>

int pthread_barrierattr_init( pthread_barrierattr_t *attr);

int pthread_barrierattr_destroy( pthread_barrierattr_t *attr);

  两个函数的返回值:若成功,返回0;否则返回错误编号;

目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障的进程内的多线程使用。

#include<pthread.h>

int pthread_barrierattr_getpshared( const pthread_barrierattr_t *restrict attr, int *restrict pshared);

int pthread_barrierattr_setpshared( pthread_barrierattr_t *attr, int pshared);

  两个函数的返回值:若成功,返回0;否则返回错误码。

进程共享实行的值可以是PTHREAD_PROCESS_SHARED(多进程中的多个线程可用),也可以是PTHREAD_PROCESS_PRIVATE(只有初始化屏障的那个进程内的多个线程可用)。

12.5 重入

如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。

支持线程安全函数的操作系统实现会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。应用程序可以用sysconf函数中传入_SC_THREAD_SAFE_FUNCTIONS参数在运行时检查是否支持线程安全函数。

如果一个函数对多个线程来说是可重入的,就说这个函数就是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。

12.6 线程特定数据

 线程特定数据(thread-specific data),也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。我们把这种数据称为线程特定数据或线程私有数据的原因是,我们希望每个线程可以访问它自己单独的数据副本,而不需要担心与其它线程的同步访问问题。

线程模型促进了进程中数据和属性的共享,但依然采用线程私有数据的第二个原因是:第一,有时候需要维护基于每线程的数据,因为线程ID并不能保证是小而连续的整数,所以就不能简单地分配一个每线程数据数组,用线程ID作为数组的索引。即使线程ID确实是小而连续的整数,我们可能还希望有一些额外的保护,防止某个线程的数据与其它线程的数据混淆。第二,它提供了让基于进程的接口适应多线程环境的机制。一个明显的例子是errno值,在进程中,errno定义为进程上下文中全局可访问的整数。系统调用和库例程在调用或执行失败时设置errno,把它作为操作失败时的附属结果。为了让线程也能够使用那些原本基于进程的系统调用和库例程,errno可被重新定义为线程私有数据,这样,一个线程做了重置errno的操作也不会影响进程中其它线程的errno值。

虽然底层的实现不能阻止线程可以访问整个地址空间的能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其它线程的线程特定数据。

在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create创建一个键。

#include<pthread.h>

int pthread_key_create(pthread_key_t *keyp, void (*destructor)( void *));

  返回值:若成功,返回0;否则返回错误码;

创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。创建新键时,每个线程的数据地址设为空值。

除了创建键以外,pthread_key_create可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的析构函数为空,就表明没有析构函数与这个键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用。同样,线程取消时,只有在最后的清理处理程序返回之后,析构函数才会被调用。如果线程调用了exit、_exit、_Exit或者abort,或者出现其它非正常的退出时,就不会调用析构函数。

线程通常使用malloc为线程特定数据分配内存。析构函数通常释放已分配的内存。如果线程在没有释放内存之前就退出了,那么这块内存就会丢失,即线程所属进程就出现了内存泄漏。

线程可以为线程特定数据分配多个键,每个键都可以有一个析构函数与它关联。每个操作系统实现可以对进程可分配的键的数量进行限制(PTHREAD_KEYS_MAX)。

线程退出时,线程特定数据的析构函数将按照操作系统实现中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程特定数据,并且把这个数据与当前的键关联起来。当所有的析构函数都调用完成后,系统会检查是否还有非空的线程特定数据值与键关联,如果有的话,再次调用析构函数。这个过程将会一直重复直到线程所有的键都为空线程特定数据值,或者已经做了PTHREAD_DESTRUCTOR_ITERATORS中定义的最大次数的尝试。

对所有的线程,我们都可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。

#include<pthread.h>

int pthread_key_delete( pthread_key_t key);

  返回值:若成功,返回0;否则返回错误编号。

注意,调用pthread_key_delete并不会激活与键关联的析构函数。要释放任何与键关联的线程特定数据值的内存,需要在应用程序中采取额外的措施。

需要确保分配的键并不会由于在初始化阶段的竞争而发生变动。解决竞争的办法是使用pthread_once。

#include<pthread.h>

pthread_once_t initflag = PTHREAD_ONCE_INIT;

int pthread_once( pthread_once_t *initflag, void (*initfn)(void));

  返回值:若成功,返回0;否则返回错误编号;

initflag必须是一个非本地变量(如全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT。

如果每个线程都调用pthread_once,系统就能保证初始化例程initfn只被调用一次。即系统首次调用pthread_once时。

键一旦创建以后,就可以通过调用pthread_setspecific函数把键和线程特定数据关联起来。可以通过pthread_getspecific函数获得线程特定数据的地址。

#include<pthread.h>

void *pthread_getspecific( pthread_key_t key);

  返回值:线程特定数据值;若没有值与该键关联,返回NULL。

int pthread_setspecific( pthread_key_t key, const void *value);

  返回值:若成功,返回0;否则返回错误编号。

如果没有线程特定数据值与键关联,pthread_getspecific将返回一个空指针,我们可以用这个返回值来确定是否需要调用pthread_setspecific。

12.7 取消选项

有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型。这两个属性影响着线程在响应pthread_cancel函数调用时所呈现的行为。

可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。

#include<pthread.h>

int pthread_setcancelstate( int state, int *oldstate);

  返回值:若成功,返回0;否则返回错误编号;

pthread_setcancelstate把当前的可取消状态设置为state,把原来的可取消状态存储在由oldstate指向的内存单元,这两步是一个原子操作。

pthread_cancel调用并不等待线程终止(发出请求取消某线程)。在默认情况下,线程在取消请求发出以后还是继续运行,直到线程到达某个取消点。取消点是线程检查它是否被取消的一个位置,如果取消了,则按照请求行事。

线程启动时默认的可取消状态是PTHREAD_CANCEL_ENABLE。当状态设为PTHREAD_CANCEL_DISABLE时,对pthread_cancel的调用并不会杀死进程。相反,取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点上对所有挂起的取消请求进行处理。

也可以自己调用pthread_testcancel函数在程序中添加自己的取消点。

#include<pthread.h>

void pthread_testcancel( void);

调用pthread_testcancel时,如果有某个取消请求正处于挂起状态,而且取消并没有置为无效,那么线程就会被取消。但是,如果取消被置为无效,pthread_testcancel调用就没有任何效果了。

我们所描述的默认的取消类型也称为推迟取消。调用pthread_cancel以后,在线程到达取消点之前,并不会出现真正的取消。可以通过调用pthread_setcanceltype来修改取消类型。

#include<pthread.h>

int pthread_setcanceltype( int type, int *oldtype);

  返回值:若成功,返回0;否则返回错误编号;

pthread_setcanceltype函数把取消类型设置为type(类型参数可以是PTHREAD_CANCEL_DEFERRED,也可以是PTHREAD_CANCEL_ASYNCHRONOUS),把原来的取消类型返回到oldtype指向的整型单元。

异步取消与推迟取消不同,因为使用异步取消时,线程可以在任意时间撤销,不是非得遇到取消点才能取消。

12.8 线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。

进程中的信号是递送到单个线程的。如果一个信号与硬件故障有关,那么该信号一般会递送到引起该事件的线程中去,而其它的信号则被发送到任意一个线程。

线程使用pthread_sigmask来修改屏蔽字。

#include<pthread.h>

int pthread_sigmask( int how, const sigset_t *restrict set, sigset_t *restrict pset);

  返回值:若成功,返回0;否则返回错误编号;

pthread_sigmask工作在线程中,而且失败时返回错误码,不再像sigpromask那样设置errno并返回-1。set参数包含线程用于修改信号屏蔽字的信号集。how参数可以取下列3个值之一:SIG_BLOCK,把信号集添加到线程信号屏蔽字中;SIG_SETMASK,用信号集提花线程屏蔽字;SIG_UNBLOCK,从线程屏蔽字中移除信号集。如果oset参数不为空,线程之前的信号屏蔽字就存储在它指向的sigset_t结构中。线程可以通过把set参数设置为NULL,并把oset参数设置为sigset_t结构的地址,来获取当前的信号屏蔽字。

线程可以通过调用sigwait等待一个或多个信号的出现:

#include<pthread.h>

int sigwait( const sigset_t *restrict set, int *restrict signo);

  返回值:若成功,返回0;否则返回错误编号;

set参数指定了线程等待的信号集。返回时,signo指向的整数将包含发送信号的数量。

如果信号集中的某个信号在sigwait调用的时候处于挂起状态,那么sigwait将无阻塞地返回。在返回之前,sigwait将从进程中移除那些处于挂起等待状态的信号。如果具体实现支持排队信号并且信号的多个实例被挂起,那么sigwait将会移除该信号的一个实例,其它的实例还要继续排队。

为了避免错误行为发生,线程在调用sigwait之前,必须阻塞那些它正在等待的信号。sigwait函数会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。

使用sigwait函数的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中。然后可以安排专用线程处理信号。这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程上下文,而非会中断线程正常执行的传统信号处理程序。

如果多个线程在sigwait的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait中返回。如果一个信号被捕获,而且一个线程正在sigwait调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号。操作系统实现可以让sigwait返回,也可以激活信号处理程序,但这两种情况不会同时发生。

要把信号发送给线程,可以调用pthread_kill。

#include<pthread.h>

int pthread_kill( pthread_t thread, int signo);

  返回值:若成功,返回0;否则返回错误编号。

可以传一个0值的signo来检查线程是否存在。如果信号的默认处理动作是终止该进程,那么把信号传递给某个线程仍然会杀死整个进程。

注意,闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。(alarm为进程设置了一个闹钟时间值,当再次调用时,会更新进程的这个值)

12.9 线程和fork

当线程调用fork时,就为子进程创建了整个进程地址空间的副本。

子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。

在子进程内部,只存在一个线程,它是由父进程中调用fork的线程的副本构成。如果父进程中的线程占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法直到它占有了哪些锁、需要释放哪些锁。

如果子进程从fork返回以后马上调用其中一个exec函数,就可以避免这样的问题。这种情况下,旧的地址空间就被丢弃,所以锁的状态无关紧要。但如果子进程需要继续做处理工作的话,这种策略就行不通,还需要使用其它的策略。

在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec之前子进程能做什么,但不涉及子进程中锁状态的问题。

要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。

#include<pthread.h>

int pthread_atfork( void (*prepare)(void), void (*parent)(void), void (*child)(void) );

  返回值:若成功,返回0;否则错误编号;

用pthread_atfork函数最多可以安装3个帮助清理锁的函数。prepare fork处理程序由父进程在fork创建子进程前调用。这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建子进程以后、返回之前在父进程上下文中调用的。这个fork处理程序的任务是对prepare fork处理程序获取的所有锁进行解锁。child fork处理程序在fork返回之前在子进程上下文中调用。与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获取的所有锁。

12.10 线程和I/O

pread和pwrite在多线程环境中很好用,因为先调用lseek再调用read/write时,由于这两步不是原子操作,所以在多个线程同时调用操作同一文件时会发生同步不安全(因为进程的一个文件描述符表项只有一个偏移量)。

猜你喜欢

转载自www.cnblogs.com/cjj-ggboy/p/12327186.html
今日推荐