线程控制与同步机制
12.1 线程配置
- 在创建线程时,如果不需要跟踪其终止状态,可以使用pthread_attr_setdetachstate方法。
- 使用pthread_attr_init进行初始化,而pthread_attr_destroy用于清理资源。
- 每个线程共享相同的虚拟地址空间。当线程栈空间不足时,可使用pthread_attr_setstack函数调整新线程的栈位置。
- 若希望更改默认栈大小但不想自行处理栈分配,可以使用pthread_attr_setstacksize。
- guardsize参数定义了线程栈末尾后用于防止溢出的额外内存大小。
12.2 同步设置
12.2.1 互斥锁配置
包括进程共享、健壮性和类型属性。
- 进程共享属性
PTHREAD_PROCESS_PRIVATE 多个线程可访问同一同步对象。
PTHREAD_PROCESS_SHARED 多个进程访问共享数据时需要同步
- 健壮属性涉及多个进程间的互斥锁。
PTHREAD_MUTEX_STALLED 当持有锁的进程结束时无需特别处理
PTHREAD_MUTEX_ROBUST 返回EOWNERDEAD
- 类型属性:
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的线程副本构成。如果父进程中的线程持有锁,子进程也将持有这些锁。
可以通过pthread_atfork注册处理函数来解决同步状态不一致的问题。
示例代码:
#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允许偏移量设定和数据读取作为一个原子操作,适用于多线程环境。所有线程共享相同的文件描述符。
