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调用返回给父进程之前运行。