APUE学习笔记——第十二章 线程控制

本章学习控制线程行为方面的详细内容,在上章中对线程属性和同步原语属性都取其默认行为,接下来将介绍同一进程中的多个线程之间如何保持数据的私有性,最后讨论基于进程的系统调用如何与线程进行交互。
1、线程限制: 
  Single Unix Specification定义了与线程操作有关的一些限制,和其他的限制一样,可以通过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函数的例子中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针。可以使用pthread_attr_t结果修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用pthread_attr_init函数来初始化pthread_attr_t结构。调用pthread_attr_init后,pthread_attr_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。如果要修改其中个人属性的值,需要调用其他的函数。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr)
int pthread_attr_destroy(pthread_attr_t *attr)
返回值:成功返回0,否则返回错误编号
POSIX.1线程属性:1)detachstate:线程的分离状态属性;2)guardsize:线程栈末尾的警戒缓冲区大小(字节数);3)stackaddr:线程栈的最低地址;4)stacksize:线程栈的大小(字节数)
上章介绍了分离线程的概率,如果对现有的某个线程的终止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时就收回它占用的资源。
如果在创建线程时就知道不需要了解线程的终止状态,则可以修改pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。可以使用phtread_attr_setdetachstate函数把线程属性detachstate设置为以下两个合法值之一:设置为PTHREAD_CREATE_DETACHED,以分离状态启动线程;设置为PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的终止状态。
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate)
int phtread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

返回值:成功返回0,否则返回错误编号。

POSIX.1线程栈属性的查询和修改:
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *attr,void* *stackaddr,size_t *stacksize)
int pthread_attr_setstack(const pthread_attr_t *attr,void* stackaddr,size_t stacksize)//中文版的课本上stacksize前多个*,应该是翻译错了
这两个函数既可以管理stackaddr线程属性,也可以管理stacksize线程属性。
int pthread_attr_getstacksize(const pthread_attr_t *attr,size_t *stacksize)
int pthread_attr_setstacksize(const pthread_attr_t *attr,size_t stacksize)
int pthread_attr_getguardsize(const pthread_attr_t *attr,size_t *guardsize)
int pthread_attr_setguardsize(const pthread_attr_t *attr,size_t guardsize)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_func(void* arg){
    pthread_attr_t attr;
    int detachstate;
    size_t guardsize;
    void* stackaddr;
    size_t stacksize;
    pthread_getattr_np(pthread_self(),&attr);
    pthread_attr_getdetachstate(&attr,&detachstate);
    pthread_attr_getstack(&attr,&stackaddr,&stacksize);
    pthread_attr_getguardsize(&attr,&guardsize);
    printf("Detachstate: %d\n",detachstate);
    printf("Stackaddr: %p\n",stackaddr);
    printf("Stacksize: %d bytes\n",stacksize);
    printf("Guardsize: %d bytes\n",guardsize);
    return (void*)0;
}

int main(){
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    //void* base = (void*)malloc(PTHREAD_STACK_MIN + 0x4000);
    //size_t size = PTHREAD_STACK_MIN + 0x4000;
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    //pthread_attr_setstack(&attr,base,size);
    pthread_create(&tid,&attr,thread_func,NULL);
    pthread_join(tid,NULL);
    sleep(2);
    exit(0);
}
Detachstate: 1
Stackaddr: 0xb6dd8000
Stacksize: 10489856 bytes
Guardsize: 4096 bytes

更多线程属性
线程还有其他的一些属性,这些属性并不在pthread_attr_t结构中表达:可取消状态、可取消类型、并发度
并发度控制着用户级线程可以映射的内核线程或进程数目。
#include <pthread.h>
int pthread_getconcurrency();//返回当前的并发度
int pthread_setconcurrency(int level)

3、同步属性
3.1、互斥量属性
用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destroy来对该结构进行回收
int pthread_mutexattr_init(pthread_mutexattr_t *attr)
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr)
进程共享互斥量
在进程中,多个线程可以访问同一个同步对象,这是默认行为,在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE
后面的章节将说明,存在这样的机制,允许相互独立的多个进程把同一个内存区域映射到它们各自独立的地址空间中,就像多个线程访问
共享数据一样,多个进程访问共享数据也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程共享的
内存区域中分配的互斥量就可以用于这些进程的同步。
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);  //查询进程共享属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared); //设置进程共享属性
类型互斥量
类型互斥量属性控制着互斥量的特性,POSIX.1定义了四种类型:
PTHREAD_MUTEX_NORMAL:标准互斥量类型,并不做任何特殊的错误检查或死锁检测
PTHREAD_MUTEX_ERRORCHECK:提供错误检查
PTHREAD_MUTEX_RECURSIVE:允许同一个线程在互斥量解锁之前对该互斥量进行多次加锁
PTHREAD_MUTEX_DEFFAULT:请求默认语义
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr,int *type); //查询类型属性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); //设置类型属性
3.2读写锁属性
与互斥量类似,但是只支持进程共享唯一属性
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);
3.3、条件变量属性
只支持进程共享属性
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr); 
int pthread_condattr_getpshared(const pthread_condattr_t *attr,int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);


4、重入

在遇到重入问题时线程与信号处理程序类似。有了信号处理程序和线程,多个控制线程在同一时间可能潜在的调用同一个函数。如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。很多函数并不是线程安全的,因为它们返回的数据是存放在静态的内存缓冲区,可以通过修改接口,要求调用者自己提供缓冲区使函数变为线程安全的。POSIX.1提供了以安全的方式管理FILE对象的方法,使用flockfile和ftrylockfile获取与给定FILE对象关联的锁。这个锁是递归锁。
void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);
为了避免标准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函数的可重入版本(不能保证异步信号安全,但是线程安全)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

extern char **environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;

static void thread_init(){
    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);
}

void 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;
	    }
            strcpy(buf,&environ[i][len+1]);//取地址复制
	    pthread_mutex_unlock(&env_mutex);
	    return;
	}
    }
    pthread_mutex_unlock(&env_mutex);
    return;
}
void* thread_func1(void* arg){
    char buf[100];
    getenv_r("HOME",buf,100);
    printf("HOME=%s\n",buf);
    pthread_exit((void*)1);
}
void* thread_func2(void* arg){
    char buf[100];
    getenv_r("SHELL",buf,100);
    printf("SHELL=%s\n",buf);
    pthread_exit((void*)2);
}

int main(){
    pthread_t tid1,tid2;
    void* ret;
    pthread_create(&tid1,NULL,thread_func1,NULL);
    pthread_create(&tid2,NULL,thread_func2,NULL);
    pthread_join(tid1,&ret);
    pthread_join(tid2,&ret);
    exit(0);
}

5、线程私有数据

线程私有数据是存储和查询与某个线程相关的数据的一种机制。设置私有数据使每个线程可以单独地访问数据副本,而不需要担心与其他线程的同步访问问题。在分配线程私有数据之前,需要创建与该数据关联的键。这个键将用于获取对线程私有数据的访问去。使用pthread_key_create创建一个键。
int pthread_key_create(pthread_key_t *keyp,void(*destructor)(void *))
除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果数据地址已经被置为非NULL数值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的destructor参数为NULL,就表明没有析构函数与键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用。
线程通常使用malloc为线程私有数据分配内存空间,析构函数通常释放已分配的内存(free),如果线程没有释放内存就退出,那么就会出现线程所属进程出现内存泄漏。
线程可以为线程私有数据分配多个键,键的数目有限制(PTHREAD_KEYS_MAX),每个键都可以有一个析构函数与它关联。
对所有的线程都可以通过调用pthread_key_delete来取消键与线程私有数据之间的关联关系。
int pthread_key_delete(pthread_key_y *key)
需要确保分配的键并不会由于在初始化阶段的竞争而发生变动。下面代码可以导致两个线程都调用pthread_key_create

void destructor(void*)
pthread_key_t key;
int init_done = 0;
int thread_func(void* arg){
    if(!init_done){
	init_done = 1;
	err = pthread_key_create(&key,destructor);
    }
    ......
}
有些线程可能看到某个键值,而其他的线程看到的可能是另一个不同的键值,这个取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once。
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void(*initfn)(void))
initflag 必须是全局变量或静态变量,而且必须初始化为PTHREAD_ONCE_INIT。
如果每个线程都调用pthread_once,系统就能保证初始化例程initfn只被调用一次,即在系统首次调用pthread_once时。
创建键时避免出现竞争的一个恰当的方法可以描述如下:
void destructor(void*);
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(void){
    pthread_key_create(&key,destructor);
}
int thread_func(void* arg){
    pthread_once(&init_done,thread_init);
}
键一旦创建,就可以通过调用pthread_setspecific函数把键和线程私有数据关联一起。通过pthread_getspecific函数获得线程私有数据的地址。
void* pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key,const void* value);

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

static pthread_key_t key;
extern char **environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;

static void thread_init(){
    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);
    pthread_key_create(&key,free);
}

char* getenv_r(const char *name,const int buflen){
    int i,len,olen;
    char *envbuf;
    pthread_once(&init_done,thread_init);
    len = strlen(name);
    pthread_mutex_lock(&env_mutex);
    envbuf = (char*)pthread_getspecific(key);
    if(envbuf == NULL){
        envbuf = (char*)malloc(100);
        if(envbuf == NULL){
            pthread_mutex_unlock(&env_mutex);
            return;
        }
        pthread_setspecific(key,envbuf);
    }
    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;
	    }
            strcpy(envbuf,&environ[i][len+1]);//取地址复制
	    pthread_mutex_unlock(&env_mutex);
	    return envbuf;
	}
    }
    pthread_mutex_unlock(&env_mutex);
    return;
}
void* thread_func1(void* arg){
    //char buf[100];
    //getenv_r("HOME",buf,100);
    printf("HOME=%s\n",getenv_r("HOME",100));
    pthread_exit((void*)1);
}
void* thread_func2(void* arg){
    //char buf[100];
    //getenv_r("SHELL",buf,100);
    printf("SHELL=%s\n",getenv_r("SHELL",100));
    pthread_exit((void*)2);
}

int main(){
    pthread_t tid1,tid2;
    void* ret;
    pthread_create(&tid1,NULL,thread_func1,NULL);
    pthread_create(&tid2,NULL,thread_func2,NULL);
    pthread_join(tid1,&ret);
    pthread_join(tid2,&ret);
    exit(0);
}

6、取消选项

取消选项包括可取消状态和可取消类型,针对线程在响应pthread_cancel函数调用时候所呈现的行为。
可取消状态取值为:PTHREAD_CANCLE_ENABLE (默认的可取消状态)或PTHREAD_CANCLE_DISABLE。
取消类型也称为延迟取消,类型可以为:PTHREAD_CANCLE_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。
通过下面函数可设置取消状态和取消类型:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void); 

7、线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。
线程中使用pthread_sigmask来等同于进程中使用sigprocmask来阻止信号发送。
#include <signal.h>
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oset);
线程中可以通过调用sigwait等待一个或多个信号发生。
int sigwait(const sigset_t *set, int *signop)
set参数指出了线程等待的信号集,signop指向的整数将作为返回值,表明发生信号的数量。
要把信号发送到进程可以调用kill函数
kill(getpid(),signo)
要把信号发送到线程,可以调用pthread_kill
int pthread_kill(pthread_t threadid,int signo)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

int quitflag = 0;
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;

void* thread_func(void* arg){
    int signo;
    puts("No thread func first output");
    while(1){
        sigwait(&mask,&signo);
        switch(signo){
	    case SIGINT:
		printf("Interrupt\n");break;
	    case SIGQUIT:
		pthread_mutex_lock(&lock);
		quitflag = 1;
		pthread_mutex_unlock(&lock);
	        pthread_cond_signal(&wait);
	    default: exit(1);
	}
    }
}

int main(){
    sigset_t omask;
    pthread_t tid;
    sigemptyset(&mask);
    sigaddset(&mask,SIGINT);
    sigaddset(&mask,SIGQUIT);
    pthread_sigmask(SIG_BLOCK,&mask,&omask);
    pthread_create(&tid,NULL,thread_func,NULL);
    puts("I'm first output, main");
    pthread_mutex_lock(&lock);//很不理解这里加锁为什么不会出现死锁
    puts("lock");
    while(quitflag == 0)
	pthread_cond_wait(&wait,&lock);
    pthread_mutex_unlock(&lock);
    puts("unlock");
    quitflag = 0;
    pthread_sigmask(SIG_SETMASK,&omask,NULL);
    sleep(2);
    exit(0);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章