12. 1 线程属性
- 如果在创建线程时就知道不需要了解线程的终止状态,就可以使用pthread_attr_setdetachstate
- pthread_attr_init对线程属性进行初始化,pthread_attr_destroy执行清理工作。
- 对于进程来说虚地址空间的大小是固定的,但对于线程来说,同样大小的虚地址空间必须被所有的线程栈共享。如果线程栈的虚地址空间都用完了,用pthread_attr_setstack函数来改变新建线程的栈位置。
- 如果希望改变默认的栈大小但又不想自己处理线程栈的分配问题,使用pthread_attr_setstacksize函数就非常有用。
- 线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小
12. 2 同步属性
12.2.1 互斥量属性
进程共享属性、健壮属性以及类型属性。
1.进程共享属性
PTHREAD_PROCESS_PRIVATE 在进程中,多个线程可以访问同一个同步对象。
PTHREAD_PROCESS_SHARED 多个进程访问共享数据通常也需要同步
2.互斥量健壮属性与在多个进程间共享的互斥量有关。
PTHREAD_MUTEX_STALLED 持有互斥量的进程终止时不需要采取特别的动作
PTHREAD_MUTEX_ROBUST 返回EOWNERDEAD
3.类型属性:
PTHREAD_MUTEX_NORMAL 重复加锁会死锁
PTHREAD_MUTEX_ERRORCHECK 重复加锁返回错误
PTHREAD_MUTEX_RECURSIVE 可重入锁
PTHREAD_MUTEX_DEFAULT 未定义
12.2.2 读写锁属性
读写锁支持的唯一属性是进程共享属性,与互斥量的进程共享属性是相同的。
12.2.3 条件变量属性
进程共享属性和时钟属性。
12.2.4 屏障属性
目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障的进程内的多线程使用。
12.3 重入
1. 线程安全:
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。
实现方式包括:
- 使用互斥量保护全局变量
- 不适用全局变量
2. 可重入:
一个函数能够被多次安全调用,称为可重入的。
分为两种情况
- 多个线程可以同时调用函数,称为对多个线程可重入。
- 同一个线程因为信号处理程序多次调用同一个函数,称为对信号处理程序可重入。
实现方式:
- 不使用全局变量,每个调用者传入自己的缓存区。
3. 两者的关系:
如果一个函数对多个线程来说是可重入的,就说这个函数就是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。
如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
必须使用递归互斥量(可重入锁)保证线程安全,还要阻止来自信号处理程序的死锁。
例子:
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include "apue.h"
extern char **environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_once = 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_mutexattr_destroy(&attr);
}
int getenv_r(const char *name, char *buf, int buflen) {
int i, len, olen;
pthread_once(&init_once, 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;
}
int main(void ) {
char* name = "LOGNAME";
char buf[1024];
int err;
if ((err = getenv_r(name, buf, 1024)) != 0)
err_exit(err, "getenv error");
printf("LOGNAME: %s\n", buf);
}
12.4 线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理程序是进程中所有线程共享的。
进程中的信号是递送到单个线程的。如果信号与硬件故障相关,那么一般会被发送到引起该事件的线程中去,而其他的信号
则被发送到任意一个线程。
如果多个线程在sigwait 的调用中因等待同一个信号而阻塞,那么在信号递送的时候就只有一个线程可以从 sigwait 中返回。
要把信号发送给进程可以调用kill,要把信号发送给线程可以调用pthread_kill。
闹钟定时器是进程资源,并且所有的线程共享相同的闹钟。
例子
#include <pthread.h>
#include <signal.h>
#include "apue.h"
sigset_t mask;
int quitflag;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait_loc = PTHREAD_COND_INITIALIZER;
void* thr_fn(void* arg) {
int err, signo;
for(;;) {
err = sigwait(&mask, &quitflag); //线程等待mask中的信号
if (err != 0)
err_exit(err, "sigwait error");
switch (signo) {
case SIGINT:
printf("quit");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&wait_loc);
return 0;
default:
printf("signo: %d", signo);
exit(1);
}
}
}
int main(void) {
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask, SIGQUIT); //设置信号屏蔽SIGQUIT,SIGINT
sigaddset(&mask, SIGINT);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
err_exit(err, "sigmask error");
err = pthread_create(&tid, NULL, thr_fn, NULL); //创建新线程
if(err != 0)
err_exit(err, "create error");
pthread_mutex_lock(&lock);
while (quitflag == 0)
pthread_cond_wait(&wait_loc, &lock); //等待条件满足
pthread_mutex_unlock(&lock);
quitflag = 0;
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0) //恢复原来的信号屏蔽字设置
err_quit("sigprocmask error");
exit(0);
}
12.5 线程和fork
当线程调用fork时,就为子进程创建了整个进程地址空间的副本。
在子进程内部只存在一个线程,是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程将同样占有这些锁。
在fork之后可能会出现同步状态不一致的问题。可以通过调用pthread_atfork函数建立fork处理程序,清除锁状态,
例子
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include "apue.h"
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void prepare(void) {
int err;
printf("preparing lock....\n");
if ((err = pthread_mutex_lock(&lock1)) != 0)
printf("cannot lock1\n");
if ((err = pthread_mutex_lock(&lock2)) != 0)
printf("cannot lock2\n");
}
void parent(void) {
int err;
printf("parent unlock....\n");
if ((err = pthread_mutex_unlock(&lock1)) != 0)
err_cont(err, "parent cannot lock1\n");
if ((err = pthread_mutex_unlock(&lock2)) != 0)
err_cont(err, "parent cannot lock2\n");
}
void child(void) {
int err;
printf("child unlock....\n");
if ((err = pthread_mutex_unlock(&lock1)) != 0)
err_cont(err, "child cannot lock1\n");
if ((err = pthread_mutex_unlock(&lock2)) != 0)
err_cont(err, "child cannot lock2\n");
}
void* thrd_fn(void* arg) {
printf("thread start\n");
pause();
return 0;
}
int main(void ) {
int err;
pid_t pid;
pthread_t tid;
//设置线程fork处理函数,分别是fork前,fork后父进程子进程执行的操作
if ((err = pthread_atfork(prepare, parent, child)) != 0)
err_exit(err, "cannot install fork handler\n");
if ((err = pthread_create(&tid, NULL, thrd_fn, NULL)) != 0)
err_exit(err, "cannot create thread\n");
sleep(2);
printf("parent about fork....\n");
if ((pid = fork()) < 0)
err_sys("fork error\n");
else if (pid == 0)
printf("child return from fork");
else
printf("parent return from fork");
exit(0);
}
12.6 线程与IO
pread和pwrite能够使偏移量的设定和数据的读取成为一个原子操作,适用于多线程环境。进程中的所有线程共享相同的文件描述符。