UNIX环境高级编程-第12章- 线程控制 - 二

12.6 线程私有属性

进程中的所有线程都可以访问进程的整个地址空间,一个线程真正拥有的唯一私有存储是处理器寄存器,甚至栈地址也能被共享,底层实现也没有阻止这种访问。但处理线程私有数据的函数可以提高线程间数据的独立性,维护基于每个线程的数据。
        在需要一个变量时,如果所有线程共享相同的值,则可以使用静态或外部数据,就像在单线程程序中那样,但通常需要互斥量来同步跨越多个线程对共享数据的存取;如果每个线程都需要一个私有变量值,则必须在某处存储所有值,并且每个线程能够定位到属于自己的值,线程私有数据机制可以做到这一点,线程私有数据避免了与其他线程同步访问的问题。

        在分配线程私有数据之前,需要创建与该数据关联的键,然后每个线程就能独立地设定或取得自己的键值。键对所有的线程是相同的,但每个线程能将它独立的键值与共享的键关联。每个线程能在任何时间为键设置它的私有值,而不会影响到其他线程的键值。线程通常使用malloc 为线程私有数据分配内存空间,析构函数通常释放以分配的内存,如果线程没有释放内存就退出了,则会造成内存泄露。

/* 线程私有数据 */  
  
/* 
 * 函数功能:为线程私有数据创建键值; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <pthread.h>  
  
int pthread_key_create(pthread_key_t *keyp, void(*destructor)(void*));  
/* 
 * 说明: 
 * 创建的键值存储在keyp所指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键 
 * 与不同的线程私有数据地址进行关联;创建新键时,每个线程的数据地址设为null; 
 * 该函数还包含一个键关联析构函数,当线程退出时,若数据地址为非null,则调用析构函数,唯一的参数就是数据地址; 
 * 若destructor为null时,表示没有析构函数; 当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但如果线程调用了exit、_exit、_Exit、abort或出现其他非正常的退出时,就不会调用析构函数。线程通常使用malloc 为线程私有数据分配内存空间,析构函数通常释放以分配的内存,如果线程没有释放内存就退出了,则会造成内存泄露。
 */  
/* 
 * 函数功能:取消线程私有数据与键之间的关联; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <pthread.h>  
  
int pthread_key_delete(pthread_key_t *key);  

      对于每个 pthread_key_t 变量只能有一个pthread_key_create 调用与之对应,如果一个键创建了两次,第二次创建的键将覆盖第一次,第一次的键和任何线程为其设置的值都将丢失。

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

        initfalg 必须是一个全局变量或静态变量,而且必须初始化为PTHREAD_ONCE_INIT。它被称之为控制变量,pthread_once 的第二个参数就是与控制变量关联的函数指针,它所指的函数没有参数。

        pthread_once 首先检查控制变量,以判断是否已经完成初始化。如果完成,pthread_once 简单地返回;否则,pthread_once 调用初始化函数。如果一个线程在初始化过程中,另外的线程也调用了pthread_once,后者将等待,直到前面的线程初始化完成。可以避免出现竞争。

        键一旦创建,就可以用过调用pthread_setspecific函数把键和线程私有数据关联起来,可以通过pthread_getspecific函数获取线程私有数据的地址。

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

测试程序:该程序的功能是输出变量名对应的值

#include "apue.h"  
#include <pthread.h>  
  
extern char **environ;  
pthread_mutex_t env_mutex;  
  
static pthread_key_t key;  
static pthread_once_t init_done = PTHREAD_ONCE_INIT;  
  
static void thread_init(void);  
char *Mgetenv(const char *name);  
void *fun1(void *arg);  
void *fun2(void *arg);  
  
int main()  
{  
    pthread_t tid1,tid2;  
    int err;  
    void *pret;  
    err = pthread_create(&tid1,NULL,fun1,NULL);  
    if(err != 0)  
        err_quit("can't create thread: %s\n", strerror(err));  
    err = pthread_create(&tid2,NULL, fun2,NULL);  
    if(err != 0)  
        err_quit("can't create thread: %s\n", strerror(err));  
    pthread_join(tid1,&pret);  
    printf("thread 1 exit code is: %d\n",(int)pret);  
    pthread_join(tid2,&pret);  
    printf("thread 2 exit code is: %d\n",(int)pret);  
    exit(0);  
}  
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_mutexattr_destroy(&attr);  
  
    pthread_key_create(&key,free);  
}  
char *Mgetenv(const char *name)  
{  
    int i,len;  
    char *envbuf;  
  
    pthread_once(&init_done,thread_init);  //如果每个线程都调用pthread_once,系统就能保证初始化例程thread_init只被调用一次,即在系统首次调用pthread_once时。可以有效避免出现竞争。
    pthread_mutex_lock(&env_mutex);  
    envbuf = (char*)pthread_getspecific(key);  
    if(envbuf == NULL)  
    {  
        envbuf = (char*)malloc(ARG_MAX);  
        if(envbuf == NULL)  
        {  
             pthread_mutex_unlock(&env_mutex);  
             return NULL;  
        }  
        pthread_setspecific(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;  
}  
void *fun1(void *arg)  
{  
    char *value;  
    printf("thread 1 start...\n");  
    value = Mgetenv("HOME");  
    printf("HOME=%s\n",value);  
    printf("thread 1 exit...\n");  
    pthread_exit((void*)1);  
}  
void *fun2(void *arg)  
{  
    char *value;  
    printf("thread 2 start...\n");  
    value = Mgetenv("SHELL");  
    printf("SHELL=%s\n",value);  
    printf("thread 2 exit...\n");  
    pthread_exit((void*)2);  
}  

程序执行结果:

[root@localhost 12]# gcc 12-1.c -lpthread
[root@localhost 12]# ./a.out
thread 1 start...
HOME=/root
thread 1 exit...
thread 2 start...
SHELL=/bin/bash
thread 2 exit...
thread 1 exit code is: 1
thread 2 exit code is: 2
[root@localhost 12]#

         使用pthread_once来确保只为将要使用的线程私有数据创建了一个键。

12.7 取消选项

  线程的取消选项有两种:可取消状态、可取消类型。这两个属性影响 pthread_cancel 函数的工作。

可取消状态

         可取消状态属性有两种状态,分别为 PTHREAD_CANCEL_ENABLE (默认) 和 PTHREAD_CANCEL_DISABLE。线程可以通过以下函数修改可取消状态:

/* 线程取消选项 */  
  
/* 
 * 函数功能:修改可取消状态属性; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <pthread.h>  
int pthread_setcancelstate(int state, int *oldstate);  
/* 
 * 说明: 
 * 该函数把可取消状态设置为state,把旧的可取消状态存放在oldstate所指的内存单元中; 
 */  
  
/* 
 * 函数功能:添加线程的取消点; 
 * 无返回值; 
 * 函数原型: 
 */  
#include <pthread.h>  
void pthread_testcancel(void);  
/* 
 * 说明: 
 * 调用该函数时,若有某个取消请求处于未决状态,而且取消并没有置为无效, 
 * 则线程就会被取消;但是若取消置为无效,则该函数调用没有任何效果; 
 */  

可取消类型

        可取消类型属性有两种类型,分别为 PTHREAD_CANCEL_DEFERRED (延时取消) 和 PTHREAD_CANCEL_ASYNCHRONOUS(异步取消)。线程可以通过以下函数修改可取消类型:

/* 
 * 函数功能:修改取消类型; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <pthread.h>  
int pthread_setcanceltype(int type, int *oldtype);  

测试程序:

#include "apue.h"  
#include <pthread.h>  
  
static void *fun1(void *arg);  
static void *fun2(void *arg);  
  
pthread_t tid1, tid2;  
int err;  
  
int main(void)  
{  
    err = pthread_create(&tid1, NULL, fun1, NULL);  
    if(err != 0)  
        err_quit("can't create thread: %s\n", strerror(err));  
  
    err = pthread_create(&tid2, NULL, fun2, NULL);  
    if(err != 0)  
        err_quit("can't create thread: %s\n", strerror(err));  
    err = pthread_detach(tid1);  
    if(err != 0)  
        err_quit("detach error: %s\n", strerror(err));  
    err = pthread_detach(tid2);  
    if(err != 0)  
        err_quit("detach error: %s\n", strerror(err));  
    exit(0);  
}  
static void *fun1(void *arg)  
{  
    err = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);  
    if(err != 0)  
        err_quit("set state error: %s\n", strerror(err));  
    printf("thread 1 starting...\n");  
    sleep(15);  
    printf("thread 1 returnting...\n");  
  
    err = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);  
    if(err != 0)  
        err_quit("set state error: %s\n", strerror(err));  
    printf("thread 1.2 starting...\n");  
    pthread_testcancel();  
    printf("thread 1.2 returnting...\n");  
    pthread_exit((void*)0);  
}  
static void *fun2(void *arg)  
{  
    printf("thread 2 starting...\n");  
    err = pthread_cancel(tid1);  
    if(err != 0)  
        err_quit("can't cancel thread 1: %s\n", strerror(err));  
    printf("thread 2 returnting...\n");  
    pthread_exit((void*)0);  
}  

12.8 线程和信号

当线程被创建时,它会继承进程的信号掩码,这个掩码就会变成线程私有的,所以我们可以设置进程的信号掩码,使其在当前进程创建的线程都会屏蔽信号。多个线程是共享进程的地址空间,每个线程对信号的处理函数是相同的,即如果某个线程修改了与某个信号相关的处理函数后,所在进程中的所有线程都必须共享这个处理函数的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择,即后来线程的处理设置会覆盖前者线程的处理设置。

        每个信号只会被传递给一个线程,即进程中的信号是传递到单个线程的,传递给哪个线程是不确定的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去。但是alarm 定时器是所有线程共享的资源,所以在多个线程中同时使用alarm 还是会互相干扰。

        在进程中可以调用 sigprocmask 来阻止信号发送,但在多线程的进程中它的行为并没有定义,它可以不做任何事情。在主线程中调用pthread_sigmask 使得所有线程都阻塞某个信号,也可以在某个线程中调用它来设置自己的掩码。

/* 线程与信号 */  
  
/* 
 * 函数功能:设置线程的信号屏蔽字; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <signal.h>  
  
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset);  
/* 
 * 说明: 
 * 该函数的功能基本上与前面介绍的在进程中设置信号屏蔽字的函数sigprocmask相同; 
 */  
  
/* 
 * 函数功能:等待一个或多个信号发生; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
int sigwait(const sigset_t *set, int *signop);  
/* 
 * 说明: 
 * set参数指出线程等待的信号集,signop指向的整数将作为返回值,表明发送信号的数量; 
 */  
  
/* 
 * 函数功能:给线程发送信号; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
int pthread_kill(pthread_t thread, int signo);  
/* 
 * 说明: 
 * signo可以是0来检查线程是否存在,若信号的默认处理动作是终止整个进程,那么把信号传递给某个线程仍然会杀死整个进程; 
 */  

        如果信号集中的某个信号在sigwait 调用的时候处于未决状态,那么sigwait 将立即无阻塞的返回,在返回之前,sigwait 将从进程中移除那些处于未决状态的信号。为了避免错误动作的发生,线程在调用sigwait 之前,必须阻塞那些它正在等待的信号。sigwait 函数会自动取消信号集的阻塞状态,直到新的信号被递送。在返回之前,sigwait 将恢复线程的信号屏蔽字。

测试程序:

#include "apue.h"  
#include <pthread.h>  
#include <signal.h>  
  
int quitflags;  
sigset_t mask;  
  
//初始化互斥量、条件变量  
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;  
  
void *thr_fun(void *arg);  
  
int main(void)  
{  
    int err;  
    sigset_t oldmask;  
    pthread_t tid;  
  
    //初始化信号集,添加两个信号SIGINT、SIGQUIT  
    sigemptyset(&mask);  
    sigaddset(&mask, SIGINT);  
    sigaddset(&mask, SIGQUIT);  
    //在主线程设置信号屏蔽字,使得所有线程都阻塞信号集的信号  
    err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask);  
    if(err != 0)  
        err_quit("SIG_BLOCK error: %s\n", strerror(err));  
    //创建新的线程  
    err = pthread_create(&tid, NULL, thr_fun, 0);  
    if(err != 0)  
        err_quit("can't create thread: %s\n", strerror(err));  
  
    //对主线程进行加锁  
    pthread_mutex_lock(&lock);  
    //等待条件变量为真  
    while(quitflags == 0)  
        pthread_cond_wait(&wait, &lock);  
    //对主线程解锁操作  
    pthread_mutex_unlock(&lock);  
  
    quitflags = 0;  
  
    //打开信号屏蔽字  
    if(sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)  
        err_sys("SIG_SETMASK error");  
    exit(0);  
}  
  
void *thr_fun(void *arg)  
{  
    int err, signo;    
    for(; ;)  
    {  
        //在新建线程中等待信号发生  
        err = sigwait(&mask, &signo);  
        if(err != 0)  
            err_quit("sigwait error: %s\n", strerror(err));    
        switch(signo)  
        {  
            case SIGINT:  
                printf("\ninterrupt\n");  
                break;  
            case SIGQUIT:  
                pthread_mutex_lock(&lock);  
                quitflags = 1;  
                pthread_mutex_unlock(&lock);  
                pthread_cond_signal(&wait);  
                return(0);  
            default:  
                printf("unexpected signal %d\n", signo);  
                exit(1);  
        }  
    }  
}  

输出结果:

^C  
interrupt  
^C  
interrupt  
^C  
interrupt  
^\  

这里并不让信号处理程序中断主控线程,而是由专门的独立控制线程进行信号处理。改动quitflag的值是在互斥量的保护下进行的,这样主控线程不会在调用pthread_cond_signal时错失唤醒调用。

12.9 线程和fork 

多线程的父进程调用 fork 函数创建子进程时,子进程继承了整个地址空间的副本。子进程里面只有一个线程,它是父进程中调用 fork 函数的线程的副本。在子进程中的线程继承了在父进程中相同的状态,即有相同的互斥量、读写锁和条件变量。如果父进程中的线程占用锁,则子进程也同样占有这些锁,只是子进程不包含占有锁的线程的副本,所以并不知道具体占有哪些锁并且需要释放哪些锁。

        如果子进程从 fork 返回之后没有立即调用 exec 函数,则需要调用 fork 处理程序清理锁状态。可以调用 pthread_atfork 函数实现清理锁状态:

/* 线程和 fork */  
  
/* 
 * 函数功能:清理锁状态; 
 * 返回值:若成功则返回0,否则返回错误编码; 
 * 函数原型: 
 */  
#include <pthread.h>  
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));  
/* 
 * 说明: 
 * 该函数最多可以安装三个帮助清理锁的函数; 
 * prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁; 
 * 
 * parent fork处理程序是在fork创建子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获取的所有锁进行解锁; 
 * 
 * child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序必须释放prepare fork处理程序获得的所有锁; 
 */  

        可以多次调用 pthread_atfork 函数从而设置多套 fork 处理程序。如果不需要使用其中某个处理程序,可以给特定的处理程序参数传入空指针,这样就不会起任何作用。使用多个 fork 处理程序时,处理程序的调用顺序并不相同。 parent   child fork  处理程序是以它们注册时的顺序进行调用的。而  prepare fork  处理程序的调用顺序与它们注册的顺序相反,这样可以允许多个模块注册它们自己的 fork 处理函数,并且保持锁的层次。

        例如,模块A调用模块B中的函数,而且每个模块有自己的一套锁。如果锁的层次是A在B之间,模块B必须在模块A之前设置fork处理程序,当父进程调用fork时,就会执行以下步骤,假设子进程在父进程之前运行。

1.调用模块A的 prepare 处理程序获取模块A的所有锁。

2.调用模块B的 prepare 处理程序获取模块B的所有锁。

3.创建子进程。

4.调用模块B中的 child 处理程序释放子进程中模块B的所有锁。

5.调用模块A中的 child 处理程序释放子进程中模块A的所有锁。

6.fork 函数返回到子进程。

7.调用模块B中的 parent 处理程序释放子进程中模块B的所有锁。

8.调用模块A中的 parent 处理程序释放子进程中模块A的所有锁。

9.fork 函数返回到父进程。

测试程序:

#include "apue.h"  
#include <pthread.h>  
#include <signal.h>  
  
pthread_mutex_t     lock1 = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_t     lock2 = PTHREAD_MUTEX_INITIALIZER;  
  
void prepare(void)  
{  
    printf("preparing locks...\n");  
    pthread_mutex_lock(&lock1);  
    pthread_mutex_lock(&lock2);  
}  
void parent(void)  
{  
    printf("parent unlocking locks...\n");  
    pthread_mutex_unlock(&lock1);  
    pthread_mutex_unlock(&lock2);  
}  
void child(void)  
{  
    printf("child unlocking locks...\n");  
    pthread_mutex_unlock(&lock1);  
    pthread_mutex_unlock(&lock2);  
}  
void* thread_func(void *arg)  
{  
    printf("thread started...\n");  
    pause();  
    return 0;  
}  
int main(void)  
{  
    pid_t       pid;  
    pthread_t   tid;  
    int err;  
  
    err = pthread_atfork(prepare,parent,child);  
    if(err != 0)  
        err_exit(err, "can't install fork handlers");  
    err = pthread_create(&tid,NULL,thread_func,NULL);  
    if(err != 0)  
        err_exit(err, "can't create thread");  
    sleep(2);  
    printf("parent about to fork.\n");  
    pid = fork();  
    if(pid == -1)  
        err_quit("fork failed: %s\n", strerror(err));  
    if(pid == 0)  
        printf("child returned from fork.\n");  
    else  
        printf("parent returned form fork.\n");  
    exit(0);  
}  

输出结果:

thread started...  
parent about to fork.  
preparing locks...  
parent unlocking locks...  
parent returned form fork.  
child unlocking locks...  
child returned from fork.  

         可以看出,preparefork处理程序在调用fork以后运行,child fork处理程序在fork调用返回到子程序之前运行,parent fork处理程序在fork调用返回给父进程之前运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值