apue 第12章 线程控制

1、引言

2、线程限制

  • PTHREAD_DESTRUCTOR_ITERATIONS 线程退出时,操作系统试图销毁线程特定数据的最大次数
  • PTHREAD_KEYS_MAX 进程可以创建的最大的键的数目
  • PTHREAD_STATC_MIN 一个线程的栈可用的最小字节书目
  • PTHREAD_THREADS_MAX 进程可以创建的最大线程数

3、线程属性

pthread的属性函数遵循以下的模式:

  1. 每个对象与一个他自己类型的属性对象关联。
  2. 有一个初始化函数
  3. 有一个销毁函数
  4. 每个属性都有一个从属性对象获取属性值的函数
  5. 每个属性都有一个设置属性值的函数

线程属性为pthread_attr_t的结构体。可以使用该结构体修改线程的默认属性,并把这些属性与创建线程联系起来。
初始化和销毁函数如下:

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

POSIX的线程属性
* detachstate 线程分离状态属性
* guardsize线程栈末尾的警戒缓冲区大小属性
* stackaddr线程栈的最低地址
* stacksize线程栈的最小长度(字节数)

如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始处于分离状态。

#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;否则,返回错误编号
  • detachstate两个状态:PTHREAD_CREATE_DETACHED,以分离状态启动线程;PTHREAD_CREATE_JIONABLE,正常启动线程

线程栈管理函数:pthread_attr_getstackpthread_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;否则,返回错误编号

线程栈大小管理函数:

#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;否则,返回错误编号

guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小

#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;否则,返回错误编号

4、同步属性

互斥量属性

互斥量属性用pthread_mutexattr_t结构体表示。
初始化和销毁函数:

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

互斥量属性有是3个属性:进程共享属性,健壮属性,类型属性。

  • 进程共享属性:PTHREAD_PROCESS_SHARED进程共享和PTHREAD_PROCESS_PRIVATE进程不共享
#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;否则,返回错误编号
  • 健壮属性:当持有互斥量的进程终止时,要解决互斥量状态恢复的问题。
    (1)PTHREAD_MUTEX_STALLED:这意味着持有互斥量的进程终止时不需要采取特别的动作。这种情况下,使用互斥量后的行为是未定义的,等待该互斥量解锁的应用程序会被有效的“拖住”。
    (2)PTHREAD_MUTEX_ROBUST:此时如果线程调用pthread_mutex_lock获取锁,且该锁被另一个进程持有,但进程终止时并没有对该锁进行解锁,此时线程会阻塞,从pthread_mutex_lock返回值为EOWNERDEAD而不是0.当我们检查线程返回值时结果为EOWNERDEAD。因此我们需要对该互斥量进行恢复。
#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_consistent函数,指明与该互斥量相关的状态在互斥量解锁之前是一直的。

#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
//返回值:若成功,返回0;否则,返回错误编号
  • 类型属性

    • PTHREAD_MUTEX_NORMAL 一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。
    • PTHREAD_MUTEX_ERRORCHAE 此互斥量类型提供错误检查。
    • PTHREAD_MUTEX_RECURSIVE 此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁。
    • PTHREAD_MUTEX_DEFAULT 此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。

    函数互斥量类型属性函数:

    
    #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;否则,返回错误编号

读写锁属性

初始化和销毁函数

#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;否则,返回错误编号

条件变量属性

初始化和销毁

#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;否则,返回错误编号
include <pthread.h>
int pthread_condattr_getclock(
                        const pthread_condattr_t *restrict attr,
                        clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,
                              clockid_t clock_id);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

屏障属性

初始化和销毁:

#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_narrierattr_t *restrict attr,
                    int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);
//两个函数的返回值:若成功,返回0;否则,返回错误编号

5、重入

如果一个函数对多线程是可重入的就说这个函数是线程安全的。如果一个函数对于异步处理信号程序是安全的,那么就说函数是异步信号安全的。

提供以线程安全的方式管理FILE对象。FILE对象关联的锁是递归锁

#include <stdio.h>
int ftrylockfile(FILE *fp);
//返回值:若成功,返回0;若不能获取锁,返回非0数值
void flockfile(FILE *fp);
void funlockfile(FILE *fp);

6、线程特定数据

线程特定数据,也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。

我们知道一个进程中的所有线程都可以访问这个进程的整个地址空间。除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。

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

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, 
                       void (*destructor)(void *));
//返回值:若成功,返回0;否则,返回错误编号
  • 创建的键存储在keyp指向的内存单元
  • 这个键可以为所有的线程共享
  • 每个线程把这个键与其他不同的线程特定数据关联
  • 可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。

取消键与线程特定数据值之间的关联关系:

#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
//返回值:若成功,返回0;否则,返回错误编号

有些线程可能看到一个键值,而其他的线程可能看到的另一个不同的键值,这取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once:

#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
//返回值:若成功,返回0;否则,返回错误编号

如果每个线程都调用pthread_once,系统就能保证初始化例程initfn只被调用一次,即系统首次调用时pthread_once时。创建键时避免出现冲突的一个正确方法如下:

void destructor(void *);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(viod)
{ err = pthread_key_create(&key,destructor); }

int threadfunc(void *arg)
{ pthread_once(&init_done , thread_init); }

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

#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
//返回值:线程特定数据值;若没有值与该键关联,返回NULL
int pthread_setspecific(pthread_key_t key, const void *value);
//返回值:若成功,返回0;否则,返回错误编号

7、取消选项

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

修改取消状态:

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
//返回值:若成功,返回0;否则,返回错误编号
  • 可取消状态设置为state(PTHREAD_CANCEL_ENABLE/PTHREAD_CANCEL_DISABLE),把原来的可取消状态存储在由oldstate指向的内存单元,这两步是一个原子操作。
  • 线程启动时默认的可取消状态是PTHREAD_CANCEL_ENABLE。
  • 当状态设置为PTHREAD_CANCEL_ENABLE时,对pthread_cancel的调用并不会杀死线程.
  • 取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点对所有挂起的取消请求进行处理。

如果应用程序很长一段时间没有调用规定的取消点。那么你可以调用pthread_testcancel函数在程序中添加自己的取消点。

#include <pthread.h>
void pthread_testcancel(void);
  • 如果有某个取消请求正处于挂起状态,而且取消并没有置为无效,那么线程就会被取消。
  • 如果取消设置被无效,pthread_testcancel调用就会有任何效果了。

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

#include <pthread.h>
int pthread_setcanceltype(int type , int *oldtype);
//返回值:若成功,返回0;否则,返回错误编号
  • 把取消类型设置为type(类型参数可以是PTHREADCANCEL_DEFERRED,也可以是PTHREAD_CANCEL_ASYNCHRONOUS),把原来的取消类型返回到oldtype指向的整型单元。

8、线程和信号

信号的处理是进程中所有线程共享。
在线程中阻止信号发送函数:

#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
                    sigset_t *restrict oset);
//返回值:若成功,返回0;否则,返回错误编号
  • set参数包含线程用于修改信号屏蔽字的信号集。
  • how参数可以取下列3个值之一:
    • SIG_BLOCK,把信号集添加到线程信号屏蔽字中,
    • SIG_SETMASK,用信号集替换线程的信号屏蔽字;
    • SIG_UNBLOCK,从线程信号屏蔽字中移除信号集。
  • 如果oset参数不为空,线程之前的信号屏蔽字就存储在它指向的sigset_t结构中。
  • 线程可以把set设为NULL,把oset参数设置为sigset_t结构的地址,来获取当前的信号屏蔽字,这种情况how参数会被忽略。

线程等待一个信号出现:

#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
//返回值:若成功,返回0;否则,返回错误编号
  • set参数指定了线程等待的信号集。
  • 返回时,signop指向的整数将包含发送信号的数量。

发送信号给线程:

#include <signal.h>
int pthread_kill(pthread_t thread,int signo);
//返回值:若成功,返回0;否则,返回错误编号

9、线程和fork

参考:http://blog.csdn.net/cywosp/article/details/27316803
在程序中fork()与多线程的协作性很差,这是POSIX系列操作系统的历史包袱。因为长期以来程序都是单线程的,fork()运转正常。当20世纪90年代初期引入线程之后,fork()的适用范围就大为缩小了。

在多线程执行的情况下调用fork()函数,仅会将发起调用的线程复制到子进程中。(子进程中该线程的ID与父进程中发起fork()调用的线程ID是一样的,因此,线程ID相同的情况有时我们需要做特殊的处理。)也就是说不能同时创建出于父进程一样多线程的子进程。其他线程均在子进程中立即停止并消失,并且不会为这些线程调用清理函数以及针对线程局部存储变量的析构函数。这将导致下列一些问题:
1. 虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留,这就造成一个危险的局面。例如:一个线程在fork()被调用前锁定了某个互斥量,且对某个全局变量的更新也做到了一半,此时fork()被调用,所有数据及状态被拷贝到子进程中,那么子进程中对该互斥量就无法解锁(因为其并非该互斥量的属主),如果再试图锁定该互斥量就会导致死锁,这是多线程编程中最不愿意看到的情况。同时,全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了。fork()函数被调用之后,子进程就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),除非函数是可重入的,而只能调用异步信号安全(async-signal-safe)的函数。fork()之后,子进程不能调用:

- malloc(3)。因为malloc()在访问全局状态时会加锁。
- 任何可能分配或释放内存的函数,包括new、map::insert()、snprintf() ……
- 任何pthreads函数。你不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步。
- printf()系列函数,因为其他线程可能恰好持有stdout/stderr的锁。
- 除了man 7 signal中明确列出的“signal安全”函数之外的任何函数。

2. 因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为(子进程)没有任何相应的引用指针。

由于这些问题,推荐在多线程程序中调用fork()的唯一情况是:其后立即调用exec()函数执行另一个程序,彻底隔断子进程与父进程的关系。由新的进程覆盖掉原有的内存,使得子进程中的所有pthreads对象消失。
对于那些必须执行fork(),而其后又无exec()紧随其后的程序来说,pthreads API提供了一种机制:fork()处理函数。利用函数pthread_atfork()来创建fork()处理函数。pthread_atfork()声明如下:

#include <pthread.h>

// Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
// @prepare 新进程产生之前被调用
// @parent 新进程产生之后在父进程被调用
// @child 新进程产生之后,在子进程被调用
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));

该函数的作用就是往进程中注册三个函数,以便在不同的阶段调用,有了这三个参数,我们就可以在对应的函数中加入对应的处理功能。同时需要注意的是,每次调用pthread_atfork()函数会将prepare添加到一个函数列表中,创建子进程之前会(按与注册次序相反的顺序)自动执行该函数列表中函数。parent与child也会被添加到一个函数列表中,在fork()返回前,分别在父子进程中自动执行(按注册的顺序)。

10、线程和IO

使用pread可以使偏移量的设定和数据的读取成为一个原子操作。

你可能感兴趣的:(apue 第12章 线程控制)