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

1、引言

2、线程限制

  • UNIX操作系统对于线程操作有一些限制。如下图所示,可以通过sysconf函数进行查询
    在这里插入图片描述
  • 下图给出了4种操作系统实现中线程限制的值。注意,表格中描述的没有确定的限制不代表无限制。
    在这里插入图片描述

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 <pthread.h>
    
    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
  • 以上几种类型互斥量的行为
    在这里插入图片描述

4、实例

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

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

  • 如果一个函数在相同的时间点可以被多个线程安全的调用,就称该函数是线程安全的。除了下图列出的函数,其他函数都是线程安全的。
    在这里插入图片描述

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

  • 很多函数不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中(如静态局部变量)。在_r函数中,通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。
    在这里插入图片描述

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

  • 如果函数对异步信号处理程序的重入是安全的,那么就说函数是异步信号安全的(图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 <limits.h>
    #include <string.h>
    
    #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 <string.h>
    #include <errno.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    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、线程特定数据

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

  • 例如errno就是线程私有数据,每个线程都拥有它自己单独的errno数据副本。一个线程重置了errno的操作也不会影响进程中其他线程的errno值。

  • 一个进程中的所有线程都可以访问这个进程的整个地址空间,除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程私有数据也不例外,虽然底层的实现不能阻止这种访问能力,但是管理线程私有数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。(即通过管理线程私有数据的函数与线程私有数据进行交互,而并非直接通过内存地址存取)

  • 在分配线程私有数据之前,需要通过pthread_key_create函数创建与该数据关联的键,这个键用于后续对线程私有数据的访问

    int pthread_key_create (pthread_key_t *key,void (*destructor) (void *))
    
  • 其中pthread_key_t代表一个键,这个键可以被进程中的所有线程使用,但是每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设为空值。

  • destructor参数是与该键关联的析构函数(可以设为NULL)。当这个线程退出时,如果数据地址已经被置为非空,那么析构函数就会被调用,它唯一的参数就是该线程私有数据地址(通常通过malloc为线程私有数据分配内存,如果析构函数不释放会导致内存泄漏)。

    • 当线程调用pthread_exit或者线程return返回,析构函数会被调用;当线程被pthread_cancel取消时,在最后的清理处理程序返回之后,析构函数被调用。
    • 如果线程调用了exit_exit_Exitabort或出现其他非正常的退出时,析构函数不会被调用。
    • 当所有的析构函数都被调用完成以后,系统会检查是否还有非空的线程私有数据值与键关联,如果有的话,再次调用析构函数。这个过程将一直重复直到超过PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数限制。
  • 对于所有的线程,通过pthread_key_delete取消键和线程特定数据值之间的关联

    int pthread_key_delete (pthread_key_t key)
    
    • 注意该函数并不会调用与键关联的析构函数。
  • pthread_once函数

    • pthread_once_t必须是非本地变量(全局变量或静态变量),并且必须初始化PTHREAD_ONCE_INIT
      pthread_once_t initflag = PTHREAD_ONCE_INIT;
      
    • 如果每个线程都调用pthread_once,系统保证__init_routine函数只被调用一次,即系统首次调用pthread_once
      int pthread_once (pthread_once_t *__once_control, void (*__init_routine) (void));
      
    • 例如有多个线程都要创建与线程私有数据关联的键时,就可以使用pthread_once来保证该键只被pthread_key_create了一次。如
      在这里插入图片描述
  • 键一旦创建以后,通过pthread_setspecific设置线程私有数据(线程私有数据本质上是一个指向一块内存的指针,或者是一个类型转换为void*的数值):参数__pointer通常指向由调用者分配的一块内存,当线程终止时,会将该指针作为参数传递给与key相关联的destructor函数

  • 通过pthread_getspecific获取线程私有数据。在使用返回的值前最好是将void*转换成原始数据类型的指针,或者转型为原始类型。

    void *pthread_getspecific (pthread_key_t __key);
    int pthread_setspecific (pthread_key_t __key,const void *__pointer);
    
  • 实例:getenv的另一个实现版本:使用线程特定数据来维护每个线程的数据缓冲区副本。

    #include <limits.h>
    #include <string.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    #define MAXSTRINGSZ	4096
    
    static pthread_key_t key;
    static pthread_once_t init_done = PTHREAD_ONCE_INIT;
    pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;
    
    extern char **environ;
    
    static void
    thread_init(void)
    {
    	/*对析构函数,使用free来释放之前有malloc分配的内存。
    	只有当线程特定数据值为非空时,析构函数才会被调用*/
    	pthread_key_create(&key, free);
    }
    
    char *
    getenv(const char *name)
    {
    	int		i, len;
    	char	*envbuf;
    	/*使用pthread_once确保只为我们将使用的线程特定数据创建一个键*/
    	pthread_once(&init_done, thread_init);
    	pthread_mutex_lock(&env_mutex);
    	/*如果pthread_getspecific返回的是NULL,就需要先分配内存缓冲区,然后再把键与该内存缓冲区关联。
    	否则,如果返回的不是空指针,就使用pthread_getspecific返回的内存缓冲区*/
    	envbuf = (char *)pthread_getspecific(key);
    	if (envbuf == NULL) {
    		envbuf = malloc(MAXSTRINGSZ);
    		if (envbuf == NULL) {
    			pthread_mutex_unlock(&env_mutex);
    			return(NULL);
    		}
    		pthread_setspecific(key, envbuf);
    	}
    	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);
    			pthread_mutex_unlock(&env_mutex);
    			return(envbuf);
    		}
    	}
    	pthread_mutex_unlock(&env_mutex);
    	return(NULL);
    }
    
    • 注意:虽然这个版本的getenv是线程安全的,但它并不是异步信号安全的。对信号处理程序而言,即使使用递归的互斥量,这个版本的getenv也不可能是可重入的,因为它调用了malloc,而malloc函数本身并不是异步信号安全的。

7、取消选项

  • 有两个线程属性没有包含在pthread_attr_t结构中:可取消状态和可取消类型。这两个属性影响着线程响应pthread_cancel函数调用时所呈现的行为
  • 可取消状态属性
    • 通过pthread_setcancelstate修改它的可取消状态,并获取原来的可取消状态。
      int pthread_setcancelstate (int __state, int *__oldstate);
      
      • PTHREAD_CANCEL_ENABLE(默认)
      • PTHREAD_CANCEL_DISABLE
  • 取消点
    • pthread_cancel调用不等待线程终止。默认情况下,被请求取消的线程在取消请求发出后继续运行,直到线程到达某个取消点

    • 取消点是线程检查它是否被取消的一个位置,如果取消了则按照请求行事。
      在这里插入图片描述在这里插入图片描述

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

    • 如果应用程序在很长一段时间都不会调用取消点函数,那么可以调用pthread_testcancel手动增加取消点

      void pthread_testcancel (void);
      
  • 可取消类型属性
    • 通过pthread_setcanceltype修改可取消类型属性
      int pthread_setcanceltype (int __type, int *__oldtype);
      
      • PTHREAD_CANCEL_DEFERRED(默认):推迟取消。调用pthread_cancel之后,在线程到达取消点之前并不会出现真正的取消。
      • PTHREAD_CANCEL_ASYNCHRONOUS:异步取消。线程可以在任意时间取消,不是非要遇到取消点才能被取消。

8、线程和信号

  • 每个线程都有自己的信号屏蔽字,但是信号处理动作是所有线程共享的。这意味着单个线程可以阻止某些信号,但是当某个线程修改了与某个给定信号相关的处理行为以后,所有线程都共享这个处理行为的改变。
  • 进程中的信号是递送到单个线程的:如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程
  • pthread_sigmask
    • 第10章中介绍了sigprocmask函数设置信号屏蔽字,但是在多线程的进程中该函数行为没有定义。因此在多线程进程中必须使用pthread_sigmask函数设置属于该线程的信号屏蔽字。
      int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
      
      • set参数:

        • 线程用于修改信号屏蔽字的信号集
      • oset参数:

        • 如果非空,则返回之前的线程信号屏蔽字
      • how参数:

        • SIG_BLOCK:把set信号集添加到信号屏蔽字中
        • SIG_SETMASK:用set信号集设置为信号屏蔽字
        • SIG_UNBLOCK:把set信号集从信号屏蔽字中移除。
  • sigwait函数
    • 线程可以通过sigwait函数等待一个或多个信号的出现
      int sigwait(const sigset_t *set, int *sig);
      
      • set参数:线程等待的信号集(在调用sigwait之前必须阻塞这些信号)
      • sig参数:包含发送信号的数量
    • sigwait函数一直阻塞直到set指定的任何一个信号被挂起为止,并从进程中移除该挂起信号。如果支持排队信号,并且信号的多个实例被挂起,那么sigwait将会移除该信号中的一个实例,其他的继续排队。
    • 为了避免错误,线程调用sigwait之前,必须阻塞那些它正在等待的信号
    • 如果多个线程在sigwait调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait返回
    • 如果一个信号被捕获(如使用sigaction建立信号处理程序),而一个线程正在sigwait调用中等待同一信号,那么这时将由操作系统决定以何种方式递送信号。可以让sigwait返回,也可以激活信号处理程序,二者只能选其一。
    • 使用sigwait函数的好处:
      • 可以简化信号处理,把异步产生的信号用同步的方式处理
      • 为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后可以安排专用线程通过sigwait函数等待处理指定信号,这样使得信号不会中断其他线程,只会在指定线程中通过sigwait函数返回。
  • 通过pthread_kill将信号发送给指定线程
    int pthread_kill (pthread_t __threadid, int __signo);
    
    • 注意,闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。所以,进程中的多个线程不可能互不干扰地使用闹钟定时器
  • 实例:在线程中,使用互斥量来保护标志(注意与10.16小节的内容进行对比)
    #include "apue.h"
    #include <pthread.h>
    
    int			quitflag;	/* set nonzero by thread */
    sigset_t	mask;
    
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER;
    
    void *
    thr_fn(void *arg)
    {
    	int err, signo;
    
    	for (;;) {
    		err = sigwait(&mask, &signo);
    		if (err != 0)
    			err_exit(err, "sigwait failed");
    		switch (signo) {
    		case SIGINT:
    			printf("\ninterrupt\n");
    			break;
    
    		case SIGQUIT:
    			/*这里加锁不会产生死锁吗?当然不会,请复习条件变量的相关内容*/
    			pthread_mutex_lock(&lock);
    			quitflag = 1;
    			pthread_mutex_unlock(&lock);
    			pthread_cond_signal(&waitloc);
    			return(0);
    
    		default:
    			printf("unexpected signal %d\n", signo);
    			exit(1);
    		}
    	}
    }
    
    int
    main(void)
    {
    	int			err;
    	sigset_t	oldmask;
    	pthread_t	tid;
    
    	sigemptyset(&mask);
    	sigaddset(&mask, SIGINT);
    	sigaddset(&mask, SIGQUIT);
    	if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
    		err_exit(err, "SIG_BLOCK error");
    
    	err = pthread_create(&tid, NULL, thr_fn, 0);
    	if (err != 0)
    		err_exit(err, "can't create thread");
    
    	pthread_mutex_lock(&lock);
    	while (quitflag == 0)
    		/*条件变量*/
    		pthread_cond_wait(&waitloc, &lock);
    	pthread_mutex_unlock(&lock);
    
    	/* SIGQUIT has been caught and is now blocked; do whatever */
    	quitflag = 0;
    
    	/* reset signal mask which unblocks SIGQUIT */
    	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    		err_sys("SIG_SETMASK error");
    	exit(0);
    }
    
    • 下面是命令行输出结果

      lh@LH_LINUX:~/桌面/apue.3e/threadctl$ ./suspend 
      ^C
      interrupt
      ^C
      interrupt
      ^C
      interrupt
      ^Z
      [3]+  已停止               ./suspend
      
    • 在主线程开始时阻塞SIGINTSIGQUIT。当创建线程进行信号处理时,新建线程继承了现有的信号屏蔽字。因为sigwait会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收,这使得我们对主线程进行编码时不用担心来自这些信号的中断。

    • 本例不用依赖信号处理程序中断主控线程,有专门的独立控制线程进行信号处理。在互斥量的保护下改动quitflag的值,这样主控线程不会在调用pthread_cond_signal时错失唤醒调用。

9、线程和fork

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

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

  • 注意,fork的子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程将同样占有这些锁。但是子进程并不包含其他占有锁的线程的副本,所以子进程不知道它占有了哪些锁,需要释放哪些锁

  • 在多线程进程中,为了避免不一致状态的问题,fork返回和子进程调用exec函数之间,子进程只能调用异步信号安全的函数。

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

    int pthread_atfork (void (*prepare) (void), void (*parent) (void),void (*child) (void));
    
    • prepare
      • 由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁(加锁)
    • parent
      • fork创建子进程之后、返回之前在父进程上下文中调用。这个fork处理程序的任务是对prepare程序获取的所有锁进行解锁。
    • child
      • fork返回之前在子进程上下文中调用。这个fork处理程序释放(解锁)prepare中获取的所有锁。
    • 该函数使得就好像出现了下列事件序列
      1. 父进程获取所有锁
      2. 子进程获取所有锁
      3. 父进程释放它的锁
      4. 子进程释放它的锁
    • 注意,parentchild程序以它们注册时的顺序调用;prepare程序调用顺序则与注册顺序相反。这样可以允许多个模块注册它们自己的fork处理程序,而且保持锁的层次
    • 假如模块A和模块B都有自己的一套锁,并且A的层次在B之前(比如模块A调用模块B)。那么就需要模块B在模块A前调用pthread_atfork函数,然后事件序列如下:
      1. 调用模块A的prepare fork处理程序获取模块A的所有锁。
      2. 调用模块B的prepare fork处理程序获取模块B的所有锁。
      3. 创建子进程。
      4. 调用模块B的child fork处理程序释放子进程中模块B的所有锁。
      5. 调用模块A的child fork处理程序释放子进程中模块A的所有锁。
      6. fork函数返回到子进程。
      7. 调用模块B的parentfork处理程序释放父进程中模块B的所有锁。
      8. 调用模块A的parentfork处理程序释放父进程中模块A的所有锁。
      9. fork函数返回到父进程。
  • 实例:描述如何使用pthread_atforkfork处理程序。

    #include "apue.h"
    #include <pthread.h>
    
    pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
    
    void
    prepare(void)
    {
    	int err;
    
    	printf("preparing locks...\n");
    	if ((err = pthread_mutex_lock(&lock1)) != 0)
    		err_cont(err, "can't lock lock1 in prepare handler");
    	if ((err = pthread_mutex_lock(&lock2)) != 0)
    		err_cont(err, "can't lock lock2 in prepare handler");
    }
    
    void
    parent(void)
    {
    	int err;
    
    	printf("parent unlocking locks...\n");
    	if ((err = pthread_mutex_unlock(&lock1)) != 0)
    		err_cont(err, "can't unlock lock1 in parent handler");
    	if ((err = pthread_mutex_unlock(&lock2)) != 0)
    		err_cont(err, "can't unlock lock2 in parent handler");
    }
    
    void
    child(void)
    {
    	int err;
    
    	printf("child unlocking locks...\n");
    	if ((err = pthread_mutex_unlock(&lock1)) != 0)
    		err_cont(err, "can't unlock lock1 in child handler");
    	if ((err = pthread_mutex_unlock(&lock2)) != 0)
    		err_cont(err, "can't unlock lock2 in child handler");
    }
    
    void *
    thr_fn(void *arg)
    {
    	printf("thread started...\n");
    	pause();
    	return(0);
    }
    
    int
    main(void)
    {
    	int			err;
    	pid_t		pid;
    	pthread_t	tid;
    
    	if ((err = pthread_atfork(prepare, parent, child)) != 0)
    		err_exit(err, "can't install fork handlers");
    	if ((err = pthread_create(&tid, NULL, thr_fn, 0)) != 0)
    		err_exit(err, "can't create thread");
    
    	sleep(2);
    	printf("parent about to fork...\n");
    
    	if ((pid = fork()) < 0)
    		err_quit("fork failed");
    	else if (pid == 0)	/* child */
    		printf("child returned from fork\n");
    	else		/* parent */
    		printf("parent returned from fork\n");
    	exit(0);
    }
    
    • 该程序定义了两个互斥量,lock1lock2prepare fork 处理程序获取这两把锁,child fork处理程序在子进程上下文中释放它们,parent fork处理程序在子进程上下文中释放它们。
    • 运行程序,命令行输出:
      lh@LH_LINUX:~/桌面/apue.3e/threadctl$ ./atfork 
      thread started...
      parent about to fork...
      preparing locks...
      parent unlocking locks...
      parent returned from fork
      child unlocking locks...
      child returned from fork
      
  • pthread_atfork函数限制:

    • 没有很好的方法对比较复杂的同步变量(如条件变量或屏障等)进行状态的重新初始化
    • 对于错误检查类型互斥量,在child处理程序试图对父进程加锁的互斥量进行解锁时可能产生错误。
    • 不能在child中清理递归互斥量,因为不知道互斥量被加锁次数
    • 如果子进程只允许调用异步信号安全函数,则child处理程序就不能清理同步对象,因为用于操作清理的所有函数都不是异步信号安全的。

10、线程和I/O

  • preadpwrite函数在多线程环境下是非常有用的,因为进程中所有的线程共享相同的文件描述符
  • 考虑两个线程,在同一时间对同一个文件描述符进行读写操作,显然这样不安全。
    在这里插入图片描述
  • 为了解决这个问题,我们可以使用pread使偏移量的设定和数据的读取成为一个原子操作。
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值