Linux系统编程学习笔记(十二)线程

线程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,可以在任何时候被取消,没有必要到达一个取消点才被取消。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值