《UNUX环境高级编程》(12)线程控制

1、引言

2、线程限制

  • UNIX操作系统对于线程操作有一些限制。如下图所示,可以通过sysconf函数进行查询
    《UNUX环境高级编程》(12)线程控制_第1张图片
  • 下图给出了4种操作系统实现中线程限制的值。注意,表格中描述的没有确定的限制不代表无限制。
    《UNUX环境高级编程》(12)线程控制_第2张图片

3、线程属性

3.1、线程属性概念

  • 对于与线程相关的对象类型,一般都有一个属性类型与之关联(如线程和线程属性关联、互斥量和互斥量属性关联):
    • 有一个初始化属性对象的函数,把属性设置为默认值
    • 有一个销毁属性对象的函数,即释放属性对象资源
    • 一个属性对象可以代表多个属性。属性对象对应用程序不透明,因此应用程序不需要了解属性对象结构实现细节,而是通过指定函数与之交互。
    • 属性对象中的每个属性都有一个设置属性值的函数,还有一个获取属性值的函数

3.2、初始化和反初始化pthread_attr_t

  • 在上一章中,通过pthread_create函数创建线程,其中pthread_attr_t是线程属性对象。如果要设置线程为默认属性,则该参数设为NULL
  • 可以通过pthread_attr_t结构修改线程属性,通过pthread_attr_init函数初始化pthread_attr_t为默认属性值。通过pthread_attr_destroy函数销毁线程属性对象
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);
    
  • 线程属性包括以下几种(部分):
    属性名称 说明
    detachstate 线程的分离状态属性
    guardsize 线程栈末尾的警戒缓冲区大小(字节)
    stackaddr 线程栈的最低地址
    stacksize 线程栈的最小长度(字节数)

3.3、线程的分离状态属性

  • 使用pthread_attr_setdetachstate函数设置线程属性对象的detachstate属性:
    • PTHREAD_CREATE_DETACHED:以分离(detach)状态启动线程
    • PTHREAD_CREATE_JOINABLE(默认):正常启动线程,应用程序可以获取线程的终止状态(通过pthread_join函数)
  • 通过pthread_attr_getdetachstate函数获取当前的detachstate线程属性
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
    
  • 实例:给出一个以分离状态创建线程的函数
    #include "apue.h"
    #include 
    
    int
    makethread(void *(*fn)(void *), void *arg)
    {
    	int				err;
    	pthread_t		tid;
    	pthread_attr_t	attr;
    	/*初始化attr为默认属性值*/
    	err = pthread_attr_init(&attr);
    	if (err != 0)
    		return(err);
    	err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    	if (err == 0)
    		err = pthread_create(&tid, &attr, fn, arg);
    	/*此例中忽略了pthread_attr_destroy函数的返回值。在这个实例中,我们对线程属性进行了合理的初始化,
    	因此pthread_attr_destroy应该不会失败。如果失败了,将难以清理,并可能造成少量的内存泄露*/
    	pthread_attr_destroy(&attr);
    	return(err);
    }
    

3.4、线程栈属性(stackaddr stacksize)

  • 可以通过sysconf(_SC_THREAD_ATTR_STACKADDR)sysconf(_SC_THREAD_ATTR_STACKSIZE)来检查系统对线程栈属性的支持情况。

1、stackaddr

  • 使用函数pthread_attr_getstackpthread_attr_setstack对线程栈属性进行获取/设置(即获取/设置线程栈的最低地址和大小)
    int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr, size_t stacksize);
    int pthread_attr_getstack(const pthread_attr_t *attr,void **stackaddr, size_t *stacksize);
    
    • 对于进程来说,虚拟地址空间大小是固定的(对于32位的操作系统,虚拟地址空间的大小为2^32B0~4GB的虚拟地址空间),因为进程中只有一个栈,所以它的大小通常不是问题。
    • 但是对于线程来说,同样大小的虚拟地址空间必须被所有的线程栈共享。如果应用程序使用很多线程,以至于这些线程栈的累计大小超过了可用的虚拟地址空间,就需要减少默认的线程栈大小
    • 如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(如递归),那么需要的栈大小可能要比默认的大
    • 如果线程栈的虚拟地址空间都用完了,可以使用mallocmmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。stackaddr参数指定的地址用作线程栈的内存范围中最低可寻址地址,stacksize为分配的缓冲区字节数
    • 注意,stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置:如果进程内存空间中栈是从高地址向低地址方向增长的,那么stackaddr将是栈的结尾地址而非开始地址。

2、stacksize

  • 通过pthread_attr_getstacksizepthread_attr_setstacksize读取/设置线程属性stacksize
    int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
    int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
    
    • 如果希望改变默认的栈大小,但是不想自己处理线程栈的分配问题(向pthread_attr_setstack那样自己设置线程栈空间),可以使用这个函数
    • 注意stacksize不能小于PTHREAD_STACK_MIN限制

3.5、线程栈末尾的警戒缓冲区大小属性

  • guardsize线程属性控制着线程栈末尾之后用以避免栈溢出的扩展内存大小。
  • 这个属性默认值由具体实现来定义,常用值时系统页大小(如4KB)。
  • 可以将guardsize线程属性设置为0,不允许属性的这种行为发生:此时不提供警戒缓冲区;同样,如果调用pthread_attr_setstack修改线程stackaddr属性,系统就认为我们自己管理栈,因此警戒缓冲区机制无效,等同于将guardsize设为0
    int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
    int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
    
    • 注意,如果修改了guardsize线程属性,操作系统可能会把它取为页的整数倍大小。
    • 如果线程的栈指针溢出到警戒缓冲区中,应用程序可能通过信号接收到出错信息。

3.6、补充:栈空间

  • 可以通过ulimit命令查看进程默认栈大小:可见栈默认大小是8MB
    $ ulimit -a
    core file size          (blocks, -c) 0
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 7721
    max locked memory       (kbytes, -l) 65536
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 65535
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 7721
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited
    
    • 所以如果我们用alloca函数在栈上申请一个很大的空间的话,应该就会发生栈越界等内存异常的程序崩溃现象。即如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误

4、同步属性

  • 类似于线程属性,线程的同步对象也有属性。在第十一章中自旋锁有一个属性称为进程共享属性。本节讨论互斥量属性、读写锁属性、条件变量属性、屏障属性

4.1、互斥量属性

  • 通过pthread_mutexattr_t结构体表示互斥量属性对象。
  • 可以通过pthread_mutexattr_init初始化互斥量属性对象,通过pthread_mutexattr_destroy反初始化互斥量属性对象。
    int pthread_mutexattr_init (pthread_mutexattr_t *__attr);
    int pthread_mutexattr_destroy (pthread_mutexattr_t *__attr);
    
    • pthread_mutexattr_initpthread_mutexattr_t初始化为默认的互斥量属性。
    • pthread_mutexattr_t互斥量属性对象包含三个属性:
      • 进程共享属性
      • 健壮属性
      • 类型属性

1、互斥量属性:进程共享属性

  • 使用pthread_mutexattr_getpsharedpthread_mutexattr_setpshared函数获取/修改pthread_mutexattr_t结构:
    int pthread_mutexattr_getpshared (const pthread_mutexattr_t *attr,int *__restrict __pshared);
    int pthread_mutexattr_setpshared (pthread_mutexattr_t *__attr,int __pshared);
    
    • PTHREAD_PROCESS_PRIVATE(默认):该进程中的线程可以访问该互斥量对象
    • PTHREAD_PROCESS_SHARED:允许相互独立的多个进程把同一个内存数据块映射到它们各自的地址空间中,即从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步(该互斥量可能位于在多个进程之间共享的共享内存对象中)

2、互斥量属性:健壮属性

  • 与在多个进程间共享的互斥量有关
  • 当持有互斥量的进程终止(即对互斥量加锁的进程终止)时,需要解决互斥量状态恢复的问题。这种情况下其他阻塞在这个锁的进程将会一致阻塞下去。
  • 通过pthread_mutexattr_getrobustpthread_mutexattr_setrobust获取/设置健壮属性值
    int pthread_mutexattr_getrobust (const pthread_mutexattr_t *__attr,int *__robustness);
    int pthread_mutexattr_setrobust (pthread_mutexattr_t *__attr,int __robustness)
    
    • PTHREAD_MUTEX_STALLED(默认):
      • 持有互斥量的进程终止时不需要采取特别的动作。这种情况下,等待该互斥量解锁的进程会被阻塞下去。
    • PTHREAD_MUTEX_ROBUST
      • 当拥有这个锁的线程挂了后,下一个尝试去获得锁的线程会获得该锁并返回EOWNWERDEAD值,新的拥有者应该再去调用pthread_mutex_consistent来保持锁状态的一致性,并解锁。否则当这个锁解锁以后,该互斥量就不再可用,其他试图获得该互斥量的线程就不能拿到该锁并返回ENOTRECOVERABLE
        int pthread_mutex_consistent(pthread_mutex_t *mutex);
        
        • 如果一个健壮的互斥量处于不一致的状态,这个函数会使它保持一致。如果互斥锁的所有者在持有互斥锁时终止,则互斥锁可能会处于不一致状态,在这种情况下,获取互斥锁的下一个所有者将成功并通过调用 pthread_mutex_lock()EOWNERDEAD 返回值通知。

3、互斥量属性:类型属性

  • 通过pthread_mutexattr_gettypepthread_mutexattr_settype获取/修改互斥量类型属性

    int pthread_mutexattr_settype (pthread_mutexattr_t *__attr, int __kind);
    int pthread_mutexattr_gettype (const pthread_mutexattr_t * __attr, int * __kind)
    
  • 类型属性表现了互斥量的锁定特性

    • PTHREAD_MUTEX_NORMAL
      • 标准互斥量类型,不做任何特殊的错误检查或死锁检测。
    • PTHREAD_MUTEX_ERRORCHECK
      • 此互斥量类型提供错误检查
    • PTHREAD_MUTEX_RECURSIVE
      • 此互斥量类型允许同一线程在互斥量解锁之前多次加锁(即递归互斥量)。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。
    • PTHREAD_MUTEX_DEFAULT
      • 此互斥量类型提供默认特性和行为。Linux操作系统把这种类型映射为PTHREAD_MUTEX_NORMAL
  • 以上几种类型互斥量的行为
    《UNUX环境高级编程》(12)线程控制_第3张图片

4、实例

  • 实例:展示递归互斥量解决并发问题的情况《UNUX环境高级编程》(12)线程控制_第4张图片
    • 假设func1func2函数的接口不能改变。我们只能把互斥量嵌入式到数据结构中,把这个数据结构的地址(x)作为参数传入。
    • 如果func1func2函数都必须操作这个结构,而且可能会有一个以上的线程同时访问该数据结构,那么func1func2必须在操作数据以前对互斥量加锁,并且该互斥量应该是递归类型的,否则会出现死锁。
  • 实例:展示了使用递归互斥量的一种替代方法
    《UNUX环境高级编程》(12)线程控制_第5张图片
    • 通过提供func2函数的私有版本,称之为func2_locked函数,可以保持func1func2函数接口不变,而且避免使用递归互斥量。
    • 要调用func2_locked函数,必须占有嵌入在数据结构中的互斥量,这个数据结构的地址是作为参数传入的。func2_locked的函数体包括func2的副本,func2现在只是获取互斥量,调用func2_locked,然后释放互斥量。
    • 提供加锁和不加锁版本的函数,在简单的情况下可行,但在更加复杂的情况下就不得不依赖递归锁。比如:库需要调用库以外的函数,而且可能会再次毁掉库中的函数时。
  • 实例:解释了有必要使用递归互斥量的另一种情况。
#include "apue.h"
#include 
#include 
#include 

extern int makethread(void *(*)(void *), void *);

struct to_info {
	void	      (*to_fn)(void *);	/* function */
	void           *to_arg;			/* argument */
	struct timespec to_wait;		/* time to wait */
};

#define SECTONSEC  1000000000	/* seconds to nanoseconds */

#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM)	nanosleep((REQ), (REM))
#endif

#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000		/* microseconds to nanoseconds */

void
clock_gettime(int id, struct timespec *tsp)
{
	struct timeval tv;

	gettimeofday(&tv, NULL);
	tsp->tv_sec = tv.tv_sec;
	tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif

void *
timeout_helper(void *arg)
{
	struct to_info	*tip;

	tip = (struct to_info *)arg;
	/*线程在时间未到时将一直等待,时间到了以后再调用请求的函数*/
	clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
	(*tip->to_fn)(tip->to_arg);
	free(arg);
	return(0);
}

/*该函数允许安排另一个函数在未来的某个时间运行*/
void
timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
	struct timespec	now;
	struct to_info	*tip;
	int				err;

	clock_gettime(CLOCK_REALTIME, &now);
	if ((when->tv_sec > now.tv_sec) ||
	  (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
		tip = malloc(sizeof(struct to_info));
		if (tip != NULL) {
			tip->to_fn = func;
			tip->to_arg = arg;
			tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
			if (when->tv_nsec >= now.tv_nsec) {
				tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
			} else {
				tip->to_wait.tv_sec--;
				tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec +
				  when->tv_nsec;
			}
			/*我们使用makethread函数以分离状态创建线程。
			因为传递给timeout函数的func函数参数在未来运行,
			所以我们不希望一直空等待线程结束。*/
			err = makethread(timeout_helper, (void *)tip);
			if (err == 0)
				return;
			else
				free(tip);
		}
	}

	/*
	 * We get here if (a) when <= now, or (b) malloc fails, or
	 * (c) we can't make a thread, so we just call the function now.
	 */
	(*func)(arg);
}

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

/*需要把retry函数安排为原子操作,retry函数试图对同一个互斥量进行加锁,所以互斥量必须是递归的*/
void
retry(void *arg)
{
	pthread_mutex_lock(&mutex);

	/* perform retry steps ... */

	pthread_mutex_unlock(&mutex);
}

int
main(void)
{
	int				err, condition, arg;
	struct timespec	when;
	/*初始化互斥量属性对象*/
	if ((err = pthread_mutexattr_init(&attr)) != 0)
		err_exit(err, "pthread_mutexattr_init failed");
	/*修改互斥量类型属性为PTHREAD_MUTEX_RECURSIVE,
	允许同一线程在互斥量解锁之前多次加锁(即递归互斥量)*/
	if ((err = pthread_mutexattr_settype(&attr,
	  PTHREAD_MUTEX_RECURSIVE)) != 0)
		err_exit(err, "can't set recursive type");
	/*使用属性attr初始化互斥量mutex*/
	if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
		err_exit(err, "can't create recursive mutex");

	/* continue processing ... */
	/*timeout的调用者需要占有互斥量来检查条件,所以对互斥量mutex进行加锁*/
	pthread_mutex_lock(&mutex);

	/*
	 * Check the condition under the protection of a lock to
	 * make the check and the call to timeout atomic.
	 */
	if (condition) {
		/*
		 * Calculate the absolute time when we want to retry.
		 */
		clock_gettime(CLOCK_REALTIME, &when);
		when.tv_sec += 10;	/* 10 seconds from now */
		timeout(&when, retry, (void *)((unsigned long)arg));
	}
	pthread_mutex_unlock(&mutex);

	/* continue processing ... */

	exit(0);
}

4.2、读写锁互斥量

  • 通过pthread_rwlockattr_t表示读写锁互斥量属性对象
    • 通过pthread_rwlockattr_initpthread_rwlock_desdroy初始化/反初始化读写锁互斥量属性对象。
      int pthread_rwlockattr_init (pthread_rwlockattr_t *__attr);
      int pthread_rwlockattr_destroy (pthread_rwlockattr_t *__attr);
      
  • 读写锁唯一支持的属性是进程共享属性。它的含义与互斥量的进程共享属性相同
    • 可以通过一组函数设置/获取读写锁的进程共享属性
      int pthread_rwlockattr_getpshared (const pthread_rwlockattr_t *__attr,int * __pshared);
      int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *__attr,int __pshared)
      
      • PTHREAD_PROCESS_SHARED
      • PTHREAD_PROCESS_PRIVATE

4.3、条件变量属性

  • 通过pthread_condattr_t类型表示条件变量属性对象。
    • 通过一对函数初始化/反初始化条件变量属性对象。
      int pthread_condattr_init (pthread_condattr_t *__attr);
      int pthread_condattr_destroy (pthread_condattr_t *__attr);
      
  • 条件变量属性有两种:
    • 进程共享属性
    • 时钟属性

1、进程共享属性

  • 它控制着条件变量可以被单进程的多个线程使用,还是可以被多进程的线程使用(与上文中其他的线程同步对象属性类似)。可以通过一组函数获取/设置条件变量的进程共享属性值。
    int pthread_condattr_getpshared (const pthread_condattr_t *__attr,int * __pshared);
    int pthread_condattr_setpshared (pthread_condattr_t *__attr,int __pshared)
    
    • PTHREAD_PROCESS_SHARED
    • PTHREAD_PROCESS_PRIVATE

2、时钟属性

  • 该属性只针对pthread_cond_timedwait函数有用,表示其超时参数采用的是哪个时钟clockid_t类型),其时钟类型可以是以下类型:
    • CLOCK_REALTIME:系统实时时间,即从1970年开始的时间
    • CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
    • CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码的CPU时间
    • CLOCK_THREAD_CPUTIME_ID:本线程到当前代码的CPU时间
  • 同样可以通过一组函数获取/设置条件变量的时钟属性
    int pthread_condattr_getclock (const pthread_condattr_t *__attr,clockid_t * __clock_id);
    int pthread_condattr_setclock (pthread_condattr_t *__attr,clockid_t __clock_id)
    
    • 注意,并没有为其他有超时等待函数的属性对象定义时钟属性。

4.4、屏障属性

  • 通过pthread_barrierattr_t表示屏障属性对象
    • 通过一组函数初始化/反初始化屏障属性对象
      int pthread_barrierattr_init (pthread_barrierattr_t *__attr);
      int pthread_barrierattr_destroy (pthread_barrierattr_t *__attr);
      
  • 屏障属性对象只有进程共享属性,它控制着屏障可以被多进程的线程属性,还是只能被初始化屏障的进程内多线程使用
    • 通过一组函数获取/设置屏障的进程共享属性
      int pthread_barrierattr_getpshared (const pthread_barrierattr_t *__attr,int *__restrict __pshared);
      int pthread_barrierattr_setpshared (pthread_barrierattr_t *__attr,int __pshared);
      
      • PTHREAD_PROCESS_SHARED
      • PTHREAD_PROCESS_PRIVATE

5、重入(线程安全函数)

  • 多个控制线程在相同的时间有可能调用相同的函数,这种情况即为重入。

  • 如果一个函数在相同的时间点可以被多个线程安全的调用,就称该函数是线程安全的。除了下图列出的函数,其他函数都是线程安全的。
    《UNUX环境高级编程》(12)线程控制_第6张图片

  • 对于一些非线程安全函数,会提供可替代的线程安全版本。这些函数的命名方式与它们的非线程安全版本的名字相似,只在最后加了_r,表明这些版本是可重入的

  • 很多函数不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中(如静态局部变量)。在_r函数中,通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。
    《UNUX环境高级编程》(12)线程控制_第7张图片

  • 注意,如果一个函数对多个线程来说是可重入的,就说明这个函数是线程安全的。但是不能说明对信号处理程序来说该函数也是可重入的

  • 如果函数对异步信号处理程序的重入是安全的,那么就说函数是异步信号安全的(图10-4介绍的就是异步信号安全函数)

  • 提供了以线程安全的方式管理FILE对象的方法,通过flockfileftrylockfile获取与FILE对象关联的锁,并且该锁是递归的。(规定所有操作FILE对象的标准I/O库函数的动作行为看起来就像是内部调用了flockfilefunlockfile

    void flockfile(FILE *filehandle);
    int ftrylockfile(FILE *filehandle);
    void funlockfile(FILE *filehandle);
    
  • 根据上文规定,标准I/O库在操作FILE对象时都会获取对应的锁,这就导致在做一次一个字符的I/O时会出现严重的性能下降(因为每次都需要获取锁和释放锁)。因此提供了不加锁版本的基于字符的标准I/O函数

    int getc_unlocked(FILE *stream);
    int getchar_unlocked(void);
    int putc_unlocked(int c, FILE *stream);
    int putchar_unlocked(int c);
    
  • 注意,尽量不要使用这几个函数,它们可能由于多个线程非同步访问是数据而引起种种问题(因为没有获得锁,因此不是互斥的访问数据)

  • 实例:展示了getenv(见7.9节)的一个可能实现,注意这个版本不是可重入的。

    #include 
    #include 
    
    #define MAXSTRINGSZ	4096
    
    static char envbuf[MAXSTRINGSZ];
    
    extern char **environ;
    
    char *
    getenv(const char *name)
    {
    	int i, len;
    
    	len = strlen(name);
    	for (i = 0; environ[i] != NULL; i++) {
    		if ((strncmp(name, environ[i], len) == 0) &&
    		  (environ[i][len] == '=')) {
    			strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1);
    			return(envbuf);
    		}
    	}
    	return(NULL);
    }
    
    • 如果两个线程同时调用这个函数,会看到不一致的结果,因为所有调用getenv的线程返回的字符串都存储在同一个静态缓冲区中。
  • 实例:给出了getenv的可重入版本,即getenv_r。它使用pthread_once函数来确保不管多少线程同时竞争getenv_r,每个进程只调用thread_init函数一次。(下一节会详细讲述pthread_once函数)

    #include 
    #include 
    #include 
    #include 
    
    extern char **environ;
    
    pthread_mutex_t env_mutex;
    
    static pthread_once_t init_done = PTHREAD_ONCE_INIT;
    
    static void
    thread_init(void)
    {
    	pthread_mutexattr_t attr;
    
    	pthread_mutexattr_init(&attr);
    	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    	pthread_mutex_init(&env_mutex, &attr);
    	pthread_mutexattr_destroy(&attr);
    }
    /*要使getenv_r可重入,需要改变接口,调用者必须提供自己的缓冲区,
    这样每个线程可以使用各自不同的缓冲区避免其他线程的干扰*/
    int
    getenv_r(const char *name, char *buf, int buflen)
    {
    	int i, len, olen;
    
    	pthread_once(&init_done, thread_init);
    	len = strlen(name);
    	/*需要在搜索请求的字符时保护环境不被修改,所以需要对互斥量加锁*/
    	pthread_mutex_lock(&env_mutex);
    	for (i = 0; environ[i] != NULL; i++) {
    		if ((strncmp(name, environ[i], len) == 0) &&
    		  (environ[i][len] == '=')) {
    			olen = strlen(&environ[i][len+1]);
    			if (olen >= buflen) {
    				pthread_mutex_unlock(&env_mutex);
    				return(ENOSPC);
    			}
    			strcpy(buf, &environ[i][len+1]);
    			pthread_mutex_unlock(&env_mutex);
    			return(0);
    		}
    	}
    	pthread_mutex_unlock(&env_mutex);
    	return(ENOENT);
    }
    
    • 可以使用读写锁,从而允许对getenv_t进行多次并发访问,但增加的并发性可能并不会在很大程度上改善程序的性能,因为:
      • 环境列表通常不会很长,所以扫描列表时并不需要长时间占有互斥量
      • getenvputenv的调用不是频繁发生的。
    • 即使可以把getenv_r变成线程安全的,这也不意味着它对信号处理程序是可重入的。
      • 如果使用的是非递归的互斥量,线程从信号处理程序中调用getenv_r就可能出现死锁。
      • 如果信号处理程序在线程执行getenv_r时中断了该线程,这时我们已经占有加锁的env_mutex,这样其他线程试图对这个互斥量的加锁就会被阻塞,最终导致线程进入死锁状态。
      • 结合上面两点,我们必须使用递归互斥量来阻止其他线程改变我们正需要的数据结构,还要阻止来自信号处理程序的死锁
    • 注意:pthread函数并不保证是异步信号安全的,所以不能把pthread函数用于其他函数,让该函数成为异步信号安全。

6、线程特定数据

你可能感兴趣的:(《UNIX环境高级编程》,java,开发语言)