线程2
线程1中我们介绍了线程和线程同步,本部分将学习线程控制的细节。我们将要看到线程属性、同步原语属性,线程私有数据。
1、线程限制:
Single Unix定义了一线线程操作的限制,和其他的限制一样,可以通过sysconf来查询。和其它的限制使用目的一样,为了应用程序的在不同操作
系统的可移植性。
一些限制:
PTHREAD_DESTRUCTOR_ITERATIONS: 销毁一个线程数据最大的尝试次数,可以通过_SC_THREAD_DESTRUCTOR_ITERATIONS作为sysconf的参数查询。
PTHREAD_KEYS_MAX: 一个进程可以创建的最大key的数量。可以通过_SC_THREAD_KEYS_MAX参数查询。
PTHREAD_STACK_MIN: 线程可以使用的最小的栈空间大小。可以通过_SC_THREAD_STACK_MIN参数查询。
PTHREAD_THREADS_MAX:一个进程可以创建的最大的线程数。可以通过_SC_THREAD_THREADS_MAX参数查询
2、线程属性:
我们在调用pthread_create,线程属性参数传递了NULL值,创建的线程使用默认的线程属性。我们如果想修改默认的线程属性,可以使用pthread_attr_init
初始化pthread_attr_t结构,然后通过pthread_setxxx来修改这个结构,最后将它作为创建线程的参数,与线程关联。
我们使用完了线程属性之后可以通过pthread_attr_destroy来销毁。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_destroy会销毁有pthread_attr_init动态申请的内存。
pthread_attr_t结构对应用程序来说是透明的,应用程序不知道内部的结构,只能通过其他的方法来查询和设置每个属性,这和面向对象的封装性一样,有助于
提高程序的移植性。
线程定义了以下属性:
detachstate: detached线程的属性。
guardsize:线程栈后保护区的大小。
stackaddr:线程栈的最低地址。
stacksize:线程栈的大小。
我们可以通过pthread_detach来让线程在退出的时候,让操作系统回收其资源。但是如果我们不需要知道线程退出的状态,我们可以在一开始创建线程的时候就
将其设置为detach状态,这个可以通过pthread_attr_setdetachstate修改detachstate来实现,可以设置两个值:PTHREAD_CREATE_DETACHED、
PTHREAD_CREATE_JOINABLE,默认是正常的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);
我们可以通过pthread_attr_getdetachstate来获得当前的detachstate,通过pthread_attr_setdetachstate来设置detachstate为:PTHREAD_CREATE_DETACHED和
PTHREAD_CREATE_JOINABLE。
例子:
创建一个detached状态的线程:
#include <pthread.h>
#include <string.h>
#include <stdio.h>
int make_detached_thread(void *(*fn)(void *), void *arg){
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(&attr);
if(err != 0){
return err;
}
err = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(err == 0)
pthread_create(&tid,&attr,fn,arg);
pthread_attr_destory(&attr);
return err;
}
我们忽略的pthread_attr_destory的返回值,因为它不会失败,即使失败我们也很难去进行清理,因为这是pthread_attr_destory是执行清理的接口。
支持线程stack属性是posix标准可选项,但是是XSI的必选。可以在编译的时候通过测试_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE
宏来检查是否支持线程堆栈属性,也可以在运行时通过sysconf检查_SC_THREAD_ATTR_STACKADDR和_SC_THREAD_ATTR_STACKSIZE
POSIX.1定义了一些接口来操作线程堆栈属性,两个老的:pthread_attr_getstackaddr和pthread_attr_setstackaddr由于具有歧义,已经被Single UNIX标准
废弃。可以通过新的函数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);
这两个函数既可以操作堆栈的地址也可以操作堆栈的大小。
进程不需要担心堆栈的大小,但是使用线程时,应该小心,因为进程的虚拟地址空间被多个线程堆栈共享,如果你使用非常多的线程,你需要减少线程栈的大小,
而如果线程申请了大量的自动变量或者调用函数使用了栈帧过深(比如深层递归),那么需要增大单个线程的栈大小。
你可以使用malloc或者mmap来申请空间,然后使用pthread_attr_setstack指令另一个栈空间,stackaddr必须是栈区的最低地址,不一定是栈首,因为硬件的结构
栈地址增长可能是从低到高还是相反,这也是pthread_attr_getstackaddr具有歧义的原因。
可以使用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);
如果你不想自己申请空间,你可以使用pthread_attr_setstacksize来设置栈大小。
guardsize是栈区末尾一端空间的大小,它用来防止栈溢出。默认的大小是PAGESIZE字节,我们可以把guardsize设置为0来禁止
这个特性。如果我们改变了stackaddr,系统假设我们将要自己维护栈空间并禁止掉栈防卫缓冲。
#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);
int pthread_attr_setguardsize(const pthread_attr_t *attr, size_t guardsize);
guardsize应设置为页大小的整数倍,如果栈指针溢出到防卫区域,应用程序将收到错误,有可能是信号。
其他的线程属性:
线程还有其他属性,但是不是由pthread_attr_t类型表示的:
1)是否可以取消状态
2)取消类型
3)并发级别
1-2见取消选项一节
并发级别控制了用户级线程和内核级线程之间的映射关系。如果实现上二者是一对一的关系,那么改变并发级别将不起作用。如果实现上是多个用户级别的线程
映射到一个内核级别的线程,可以使用pthread_setconcurrency来向操作系统提供一个hint,让其满足期望的并发度。
#include <phtread.h>
int pthread_getconcurrency(void);
int pthread_setconcurrency(int level);
pthread_setconcurrency返回当前的并发级别,如果操作系统控制并发级别(比如没有调用过pthread_setconcurrency)。应用程序可以通过把level设置为0,来
撤销上一次调用。
3、同步属性
像线程属性一样,同步对象互斥量、读写锁、条件变量,也有他们的属性。
1)互斥量属性:
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);
进程共享属性和类型属性是我们感兴趣的两个互斥量属性。进程共享属性是POSIX可选的属性,可以通过测试_POSIX_THREAD_PROCESS_SHARED是否定义来检测是否支持。
也可以在运行时传_SC_THREAD_PROCESS_SHARED到sysconf来测试。Single Unix规范则要求支持此选项。
共享属性可设为两个值:PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED两个值。默认是PTHREAD_PROCESS_PRIVATE,进程及进程内线程只能访问本进程内的互斥量。
PTHREAD_PROCESS_SHARED多个进程可以共享此互斥量。
可以通过pthread_mutexattr_getshared和pthread_mutexattr_setshared来查询和设置进程共享属性:
#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);
类型属性:
POSIX.1定义了四种互斥量类型属性:
PTHREAD_MUTEX_NORMAL:不进行特殊的错误检查和死锁检测
PTHREAD_MUTEX_ERRORCHECK:进行错误检查
PTHREAD_MUTEX_RECURSIVE:允许同一线程在未释放锁之前多次再加锁
PTHREAD_MUTEX_DEFAULT:依赖于实现,具体映射到以上三种中的一种
可以通过pthread_mutexattr_gettype和pthread_mutexattr_settype来查询和修改type属性:
#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);
递归互斥量主要用处是:现有的单线程结构应用到多线程环境中,同时为了兼容不能改变接口。
2)读写锁属性:
和互斥量一样,读写锁也使用pthread_rwlockattr_init和pthread_rwlockattr_destroy两个方法来创建和销毁读写锁属性。
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
读写锁唯一支持的属性有进程共享属性,这个和互斥量类似:
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared)
int pthread_rwlockattr_setpshared(phtread_rwlockattr_t *attr, int pshared),
3)条件变量属性:
条件变量也有一对初始化和销毁的方法:
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
和其他的同步原语一样,支持进程共享属性:
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr *attr, int pshared);
4、可重入性:
我们已经在信号处理那块讨论了可重入函数。对于可重入来说,线程和信号处理函数类似。一个函数如果可以安全的被多个线程同时调用,
则称之为线程安全的。如果一个函数可重入,那么它是线程安全的,但使用了非可重入函数(例如malloc(3))的线程也能通过互斥等同步
机制实现线程安全。所以不能从线程安全函数是可重入函数的结果,特别是线程安全并不能保证异步信号安全。
POSIX还提供了以线程安全的方式访问FILE对象的方法,即针对FILE对象的锁同步机制:
#include <stdio.h>
int ftrylockfile(FILE *fp);
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
标准I/O例程需要他们自己的锁。但如果我们在每次一个字符的I/O操作使用锁会产生严重的性能问题。为了避免上述问题,为基于字符的标准I/O,
提供了一下四个unlock版本。
#include <stdio.h>
int getchar_unlocked(void);
int getc_unklocked(void);
int putchar_unlocked(void);
int putc_unlocked(void);
这四个函数需要在flockfile或者ftrylockfile内部使用。否则会有不可预测的结果。
线程安全的getenv例子:
#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_mutex_attr_destroy(&attr);
}
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;
}
5、线程私有数据
线程私有数据是用来存储和查找与线程相关数据的一种机制。之所以叫私有数据,是因为每一个线程都访问它自己的那份数据的拷贝,不需要考虑
和其他线程同步访问数据。
使用它的原因:
1、有时候我们需要维护每个线程一份数据。虽然我们可以使用线程id作为可以的hash来做,但是还需要互斥访问和保护,防止其他线程访问该线程
的私有数据。
2、提供了将基于进程的结构适配成多线程环境的方法。比如errno
在申请和线程相关的数据之前,我们需要创建key,来和这个数据关联,我们以后会使用这个key来访问这个数据。
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp; void (*destructor)(void *));
多个线程可以使用同一个key,但是每个线程要关联不同的该线程私有的数据地址。当线程退出时(pthread_exit,return),数据的地址被设置为NULL,
参数二指定的destructor将会被调用,并将该私有数据地址作为参数传递到该函数中。
但是如果线程调用的是exit,_exit,_Exit后者abort或者其他不正常退出方法,destructor不会被调用。线程经常使用malloc申请线程私有数据,这个
destructor用于释放该数据申请的内存。
一个线程可以用多个key来关联线程的私有数据,可以使用相同的destructor,也可以不同。
如果我们想将该key和关联的私有数据断开,那么可以通过调用pthread_key_delete来完成:
#include <pthread.h>
int pthread_key_delete(pthread_key_t *key);
调用这个行数不会触发上面我们说的destructor的调用,我们需要采取其他的措施来释放。
我们需要保证key不被改变。
我们可能使用下面的错误的代码来做初始化,以保证不被多次初始化:
void destructor(void *);
pthread_key_t key;
int init_done = 0;
int threadfunc(void *arg){
if(! init_done){
init_done = 1;
err = pthread_key_create(&key,destructor);
}
//...
}
这个由于线程条件竞争,可能被初始化多次或者看到不一致的状态。使用pthread_once可以解决这个问题:
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
参数initflag必须是非局部变量,并且被初始化为PTHREAD_ONCE_INIT。
pthread_once可以保证初始化例程initfn只被调用一次:
void destructor(void *);
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(void){
err = pthread_key_create(&key,destructor);
}
int threadfunc(void *arg){
pthread_once(&init_done,thread_init);
//...
}
一旦key被创建,我们就可以通过调用pthread_setspecific将key和私有数据关联,可以通过调用pthread_getspecific来通过key获得
关联的数据。
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
我们前面通过改变getenv_r来改造了一个线程安全的获得环境变量的函数,如果我们不能够修改函数接口,那该如何去做?我们可以通过
使用线程私有数据来维护每个线程一个数据buffer的拷贝。
例子:
#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
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){
pthread_key_create(&key,free);
}
char *getenv(const char *name){
int i, len;
char *envbuf;
pthread_once(&init_done, thread_init);
pthread_mutex_lock(&env_mutex);
envbuf = (char *)pthread_getspecific(key);
if(envbuf == NULL){
envbuf = malloc(ARG_MAX);
if(envbuf == null){
pthread_mutex_unlock(&env_mutex);
return NULL;
}
pthread_seetspecific(key,envbuf);
}
len = strlen(name);
for(i = 0; environ[i] != NULL; i++){
if((strncmp(name,environ[i],len) == 0) && (environ[i][len] == '=')){
strcpy(envbuf, &environ[i][len+1]);
pthread_mutex_unlock(&env_mutex);
return envbuf);
}
}
pthread_mutex_unlock(&env_mutex);
return NULL;
}
6、取消选项
两个属性没有包含在pthread_attr_t结构中:可取消状态和取消类型。取消状态可以取:PTHRAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。
可以通过调用pthread_setcancelstate:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
在一个原子操作中,将当前可取消状态设置成state,并将以前的状态设置到oldstate。
默认pthread_cancel并不使线程立即停止,线程会继续运行到取消点,一个取消点是线程检查是否被取消的地方。一个线程在调用一下函数时,
会检查是否有退出请求:
引用
accept、mq_timedsend、putpmsg、sigsuspend、aio_suspend、msgrcv、pwrite、sigtimedwait、clock_nanosleep、msgsnd、read
sigwait、close、msync、readv、sigwaitinfo、connect、nanosleep、recv、sleep 、creat、open、recvfrom、system 、fcntl2
pause、recvmsg、tcdrain 、fsync、poll、select、usleep、getmsg、pread、sem_timedwait、wait、getpmsg、pthread_cond_timedwait
sem_wait、waitid 、lockf、pthread_cond_wait、send、waitpid、mq_receive、pthread_join、sendmsg、write 、mq_send
pthread_testcancel、sendto、writev 、mq_timedreceive、putmsg、sigpause
如果应用程序很长时间没有没有调用上面所列的函数,那么可以通过pthread_testcancel在程序中添加自己的取消点。
#include <pthread.h>
void pthread_testcancel(void);
我们可以通过pthread_setcanceltype来改变取消的类型,而不是保持上面说的默认的行为:
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
type可以被设置为:PTHREAD_CANCEL_DEFERRED或者PTHREAD_CANCEL_ASYNCHRONOUS.返回以前的type,设置到oldtype中。
异步取消PTHREAD_CANCEL_ASYNCHRONOUS不同于PTHREAD_CANCEL_DEFERRED,可以在任何时候被取消,没有必要到达一个取消点才被取消。