线程2

5 篇文章 0 订阅
线程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来销毁。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_attr_init(pthread_attr_t *attr);  
  4. 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。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);  
  4. 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状态的线程: 
Cpp代码  
  1. #include <pthread.h>  
  2. #include <string.h>  
  3. #include <stdio.h>  
  4.   
  5. int make_detached_thread(void *(*fn)(void *), void *arg){  
  6.     int err;  
  7.     pthread_t tid;  
  8.     pthread_attr_t attr;  
  9.       
  10.     err = pthread_attr_init(&attr);  
  11.     if(err != 0){  
  12.         return err;  
  13.     }     
  14.     err = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);  
  15.     if(err == 0)  
  16.         pthread_create(&tid,&attr,fn,arg);  
  17.     pthread_attr_destory(&attr);  
  18.     return err;  
  19. }  

我们忽略的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来做。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_attr_getstack(const pthread_attr_t *restrict attr,void ** restrict stackaddr, size_t *restrict stacksize);  
  4. 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。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);  
  4. int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);  

如果你不想自己申请空间,你可以使用pthread_attr_setstacksize来设置栈大小。 
guardsize是栈区末尾一端空间的大小,它用来防止栈溢出。默认的大小是PAGESIZE字节,我们可以把guardsize设置为0来禁止 
这个特性。如果我们改变了stackaddr,系统假设我们将要自己维护栈空间并禁止掉栈防卫缓冲。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, size_t *restrict guardsize);  
  4. int pthread_attr_setguardsize(const pthread_attr_t *attr, size_t guardsize);  

guardsize应设置为页大小的整数倍,如果栈指针溢出到防卫区域,应用程序将收到错误,有可能是信号。 

其他的线程属性: 
线程还有其他属性,但是不是由pthread_attr_t类型表示的: 
1)是否可以取消状态 
2)取消类型 
3)并发级别 
1-2见取消选项一节 
并发级别控制了用户级线程和内核级线程之间的映射关系。如果实现上二者是一对一的关系,那么改变并发级别将不起作用。如果实现上是多个用户级别的线程 
映射到一个内核级别的线程,可以使用pthread_setconcurrency来向操作系统提供一个hint,让其满足期望的并发度。 
Cpp代码  
  1. #include <phtread.h>  
  2.   
  3. int pthread_getconcurrency(void);  
  4. int pthread_setconcurrency(int level);  

pthread_setconcurrency返回当前的并发级别,如果操作系统控制并发级别(比如没有调用过pthread_setconcurrency)。应用程序可以通过把level设置为0,来 
撤销上一次调用。 

3、同步属性 
像线程属性一样,同步对象互斥量、读写锁、条件变量,也有他们的属性。 
1)互斥量属性: 
pthread_mutexattr_init初始化pthread_mutexattr_t结构,pthread_mutexattr_destroy销毁该结构。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_mutexattr_init(pthread_mutexattr_t *attr);  
  4. 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来查询和设置进程共享属性: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);  
  4. 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属性: 

Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);  
  4. int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);  

递归互斥量主要用处是:现有的单线程结构应用到多线程环境中,同时为了兼容不能改变接口。 
2)读写锁属性: 
和互斥量一样,读写锁也使用pthread_rwlockattr_init和pthread_rwlockattr_destroy两个方法来创建和销毁读写锁属性。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);  
  4. int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);  

读写锁唯一支持的属性有进程共享属性,这个和互斥量类似: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared)  
  4. int pthread_rwlockattr_setpshared(phtread_rwlockattr_t *attr, int pshared),  

3)条件变量属性: 
条件变量也有一对初始化和销毁的方法: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_condattr_init(pthread_condattr_t *attr);  
  4. int pthread_condattr_destroy(pthread_condattr_t *attr);  

和其他的同步原语一样,支持进程共享属性: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);  
  4. int pthread_condattr_setpshared(pthread_condattr *attr, int pshared);  

4、可重入性: 
我们已经在信号处理那块讨论了可重入函数。对于可重入来说,线程和信号处理函数类似。一个函数如果可以安全的被多个线程同时调用, 
则称之为线程安全的。如果一个函数可重入,那么它是线程安全的,但使用了非可重入函数(例如malloc(3))的线程也能通过互斥等同步 
机制实现线程安全。所以不能从线程安全函数是可重入函数的结果,特别是线程安全并不能保证异步信号安全。 

POSIX还提供了以线程安全的方式访问FILE对象的方法,即针对FILE对象的锁同步机制: 
Cpp代码  
  1. #include <stdio.h>  
  2.   
  3. int ftrylockfile(FILE *fp);  
  4. void flockfile(FILE *fp);  
  5. void funlockfile(FILE *fp);  

标准I/O例程需要他们自己的锁。但如果我们在每次一个字符的I/O操作使用锁会产生严重的性能问题。为了避免上述问题,为基于字符的标准I/O, 
提供了一下四个unlock版本。 
Cpp代码  
  1. #include <stdio.h>  
  2.   
  3. int getchar_unlocked(void);  
  4. int getc_unklocked(void);  
  5.   
  6. int putchar_unlocked(void);  
  7. int putc_unlocked(void);  

这四个函数需要在flockfile或者ftrylockfile内部使用。否则会有不可预测的结果。 
线程安全的getenv例子: 
Cpp代码  
  1. #include <string.h>  
  2. #include <errno.h>  
  3. #include <pthread.h>  
  4. #include <stdlib.h>  
  5.   
  6. extern char **environ;  
  7.   
  8. pthread_mutex_t env_mutex;  
  9. static pthread_once_t init_done = PTHREAD_ONCE_INIT;  
  10.   
  11. static void thread_init(void){  
  12.     pthread_mutexattr_t attr;  
  13.     pthread_mutexattr_init(&attr);  
  14.     pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);  
  15.     pthread_mutex_init(&env_mutex,&attr);  
  16.     pthread_mutex_attr_destroy(&attr);  
  17. }  
  18.   
  19. int getenv_r(const char *name, char *buf, int buflen){  
  20.     int i, len, olen;  
  21.     pthread_once(&init_done,thread_init);  
  22.     len = strlen(name);  
  23.     pthread_mutex_lock(&env_mutex);  
  24.     for(i = 0; environ[i] != NULL; i++){  
  25.         if((strncmp(name,environ[i],len) == 0) && (environ[i][len] == '=')){  
  26.             olen = strlen(&environ[i][len+1];  
  27.             if(olen >= buflen){  
  28.                 pthread_mutex_unlock(&env_mutex);  
  29.                 return ENOSPC;  
  30.             }  
  31.             strcpy(buf,&environ[i][len+1];  
  32.             pthread_mutex_unlock(&env_mutex);  
  33.             return 0;  
  34.         }  
  35.     }  
  36.     pthread_mutex_unlock(&env_mutex);  
  37.     return ENOENT;  
  38. }  

5、线程私有数据 
线程私有数据是用来存储和查找与线程相关数据的一种机制。之所以叫私有数据,是因为每一个线程都访问它自己的那份数据的拷贝,不需要考虑 
和其他线程同步访问数据。 
使用它的原因: 
1、有时候我们需要维护每个线程一份数据。虽然我们可以使用线程id作为可以的hash来做,但是还需要互斥访问和保护,防止其他线程访问该线程 
的私有数据。 
2、提供了将基于进程的结构适配成多线程环境的方法。比如errno 
在申请和线程相关的数据之前,我们需要创建key,来和这个数据关联,我们以后会使用这个key来访问这个数据。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. 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来完成: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_key_delete(pthread_key_t *key);  

调用这个行数不会触发上面我们说的destructor的调用,我们需要采取其他的措施来释放。 
我们需要保证key不被改变。 
我们可能使用下面的错误的代码来做初始化,以保证不被多次初始化: 
Cpp代码  
  1. void destructor(void *);  
  2.   
  3. pthread_key_t key;  
  4. int init_done = 0;  
  5.   
  6. int threadfunc(void *arg){  
  7.     if(! init_done){  
  8.         init_done = 1;  
  9.         err = pthread_key_create(&key,destructor);  
  10.     }  
  11.     //...  
  12. }  

这个由于线程条件竞争,可能被初始化多次或者看到不一致的状态。使用pthread_once可以解决这个问题: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. pthread_once_t initflag = PTHREAD_ONCE_INIT;  
  4. int pthread_once(pthread_once_t *initflag, void (*initfn)(void));  

参数initflag必须是非局部变量,并且被初始化为PTHREAD_ONCE_INIT。 
pthread_once可以保证初始化例程initfn只被调用一次: 
Cpp代码  
  1. void destructor(void *);  
  2. pthread_key_t key;  
  3. pthread_once_t init_done = PTHREAD_ONCE_INIT;  
  4.   
  5. void thread_init(void){  
  6.     err = pthread_key_create(&key,destructor);  
  7. }  
  8.   
  9. int threadfunc(void *arg){  
  10.     pthread_once(&init_done,thread_init);  
  11.     //...  
  12. }  

一旦key被创建,我们就可以通过调用pthread_setspecific将key和私有数据关联,可以通过调用pthread_getspecific来通过key获得 
关联的数据。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. void *pthread_getspecific(pthread_key_t key);  
  4. int pthread_setspecific(pthread_key_t key, const void *value);  

我们前面通过改变getenv_r来改造了一个线程安全的获得环境变量的函数,如果我们不能够修改函数接口,那该如何去做?我们可以通过 
使用线程私有数据来维护每个线程一个数据buffer的拷贝。 
例子: 
Cpp代码  
  1. #include <limits.h>  
  2. #include <string.h>  
  3. #include <pthread.h>  
  4. #include <stdlib.h>  
  5.   
  6. static pthread_key_t key;  
  7. static pthread_once_t init_done = PTHREAD_ONCE_INIT;  
  8. pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;  
  9.   
  10. extern char **environ;  
  11.   
  12. static void thread_init(void){  
  13.     pthread_key_create(&key,free);  
  14. }  
  15.   
  16. char *getenv(const char *name){  
  17.     int i, len;  
  18.     char *envbuf;  
  19.   
  20.     pthread_once(&init_done, thread_init);  
  21.     pthread_mutex_lock(&env_mutex);  
  22.     envbuf = (char *)pthread_getspecific(key);  
  23.     if(envbuf == NULL){  
  24.         envbuf = malloc(ARG_MAX);  
  25.         if(envbuf == null){  
  26.             pthread_mutex_unlock(&env_mutex);  
  27.             return NULL;  
  28.         }  
  29.         pthread_seetspecific(key,envbuf);  
  30.     }  
  31.     len = strlen(name);  
  32.     for(i = 0; environ[i] != NULL; i++){  
  33.         if((strncmp(name,environ[i],len) == 0) && (environ[i][len] == '=')){  
  34.             strcpy(envbuf, &environ[i][len+1]);  
  35.             pthread_mutex_unlock(&env_mutex);  
  36.             return envbuf);  
  37.         }  
  38.     }  
  39.     pthread_mutex_unlock(&env_mutex);  
  40.     return NULL;  
  41. }  

6、取消选项 
两个属性没有包含在pthread_attr_t结构中:可取消状态和取消类型。取消状态可以取:PTHRAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。 
可以通过调用pthread_setcancelstate: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. 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在程序中添加自己的取消点。 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. void pthread_testcancel(void);  


我们可以通过pthread_setcanceltype来改变取消的类型,而不是保持上面说的默认的行为: 
Cpp代码  
  1. #include <pthread.h>  
  2.   
  3. int pthread_setcanceltype(int type, int *oldtype);  

type可以被设置为:PTHREAD_CANCEL_DEFERRED或者PTHREAD_CANCEL_ASYNCHRONOUS.返回以前的type,设置到oldtype中。 
异步取消PTHREAD_CANCEL_ASYNCHRONOUS不同于PTHREAD_CANCEL_DEFERRED,可以在任何时候被取消,没有必要到达一个取消点才被取消。  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值