概述
在传统的Unix模型中,当一个进程需要另一个实体来完成某事时,它就fork一个子进程并让子进程去执行处理。Unix上的大多数网络服务器程序就是这么编写的:父进程accept一个连接,fork一个子进程,该子进程处理与该连接对端的客户之间的通信。
尽管如此,但是fork调用却存在一些问题:
- fork是昂贵的。它要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符等等。当今的实现使用称为写时复制的技术,用以避免在子进程切实需要自己的副本之前把父进程的数据空间复制到子进程;
- fork返回之后父子进程之间信息的传递需要进程间通信机制。调用fork之前父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始运行。然而从子进程往父进程返回信息却比较费力;
同一进程内的所有线程共享相同的全局内存,而且还共享:
- 进程指令
- 大多数数据
- 打开的文件(即描述符)
- 信号处理函数和信号处置
- 当前工作目录
- 用户ID和组ID
不过每个线程都有各自的:
- 线程ID
- 寄存器集合,包括程序计数器PC和栈指针
- 栈(用于存放局部变量和返回地址)
- errno
- 信号掩码
- 优先级
-
基本线程函数:创建和终止
-
pthread_create函数
当一个程序由exec启动执行时,称为初始线程或主线程的单个线程就创建了。其余线程则由pthread_create函数创建。
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void *), void *arg);
//若成功返回0,若出错则为正的Exxx值
一个进程内的每个线程都由一个线程ID标识,其数据类型为pthread_t(往往是unsigned int)。如果新的线程成功创建,其ID就通过tid指针返回。
【参数解析】
- pthread_attr_t:指定线程的属性,例如优先级、初始栈大小、是否成为一个守护线程;
- func:函数地址;
- arg:函数的唯一调用参数,若给函数传递多个参数,则需要把参数打包成一个结构,然后传入;
【注】
pthread函数不设置errno,若超过某个系统限制而不能创建新线程,则会返回EAGAIN。
-
pthread_join函数
通过该函数等待一个给定线程终止。
#include <pthread.h>
int pthread_join(pthread_t *tid, void **status);
//若成功则为0,若出错则为正的Exxx值
【参数解析】
- tid:指定要等待线程的tid;
- status:来自所等待线程的返回值(一个指向某个对象的指针)将存入由status指向的位置;
-
pthread_self函数
每个线程都有一个在所属进程内标识自身的ID,使用该函数去获取自身的线程ID。
#include <pthread.h>
pthread_t pthread_self(void);
//返回:调用线程的线程ID
-
pthread_detach函数
一个线程或者是可汇合的(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关资源都被释放,我们不能等待它们终止。
该函数把指定线程转变为脱离状态。
#include <pthread.h>
int pthread_detach(pthread_t tid);
//返回:若成功则为0,若出错则为正的Exxx值
本函数通常由想让自己脱离的线程调用,如下:
pthread_detach(pthread_self);
-
pthread_exit函数
让一个线程终止的方法之一是调用pthread_exit。
#include <pthread.h>
void pthread_exit(void *status);
//不返回到调用者
-
例子:使用线程的TCP回射服务器程序
#include "unpthread.h"
static void *doit(void *);
int main(int argc, char **argv){
int listenfd, *iptr;
pthread_t tid;
socklen_t addrlen, len;
struct sockaddr *cliaddr;
if(argc == 2)
listen = tcp_listen(NULL, argv[1], &addrlen);
else if(argc == 3)
listen = tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: tcpserv01 [ <host> | <service or port> ]");
cliaddr = malloc(addrlen);
for( ; ; ){
len = addrlen;
iptr = malloc(sizeof(int));
*iptr = accept(listenfd, cliaddr, &len);
pthread_create(&tid, NULL, &doit, iptr);
}
}
static void* doit(void *arg){
int connfd;
connfd = *((int *) arg);
free(arg);
pthread_detach(pthread_self());
str_echo(connfd);
close(connfd);
return NULL;
}
以下是线程安全函数:
处理线程特定数据时通常首先调用以下两个函数:
#include <pthread.h>
int pthread_once(pthread_once_t *onceptr, void (*init) (void));
int pthread_key_create(pthread_key_t *keyptr, void (*destrcutor) (void *value));
//返回:若成功则为0,若出错则为正的Exxx值
每当一个使用线程特定数据的函数被调用时,pthread_once通常转而被该函数调用,不过pthread_once使用由onceptr参数指向的变量中的值,确保init参数所指的函数在进程范围内只被调用一次;
在进程范围内对于一个给定键,pthread_key_create只能被调用一次。所创建的键通过keyptr指针参数返回,如果destructor指针参数不为空指针,它所指的函数将由为该键存放过某个值的每个线程在终止时调用;
这两个函数的典型用法如下:
pthread_key_t r1_key;
pthread_once_t ri_once = PTHREAD_ONCE_INIT;
void readline_destructor(void *ptr){
free(ptr);
}
void readline_once(void){
pthread_key_create(&r1_key, readline_destructor);
}
ssize_t readline(...){
...
pthread_once(&r1_once, readline_once);
if((ptr = pthread_getspecific(r1_key)) == NULL){
ptr = malloc( ... );
pthread_setspecific(r1_key, ptr);
}
}
每次readline被调用时,它都调用pthread_once。pthread_once使用由其onceptr参数指向的值(变量r1_once的内容)确保由init参数指向的函数只被调用一次。初始化函数readline_once创建一个线程特定数据键存放在r1_key中,readline随后在pthread_getspecific和pthread_setspecific调用中使用这个键。
以下两个函数(即上面加粗加红的函数名)分别用于获取和存放于某个键关联的值:
#include <pthread.h>
void* pthread_getspecific(pthread_key_t key);
//返回:指向线程特定数据的指针(有可能是一个空指针)
int pthread_setspecific(pthread_key_t key, const void *value);
//返回:若成功则为0,若出错则为正的Exxx值
-
例子:使用线程特定数据的readline函数
#include "unpthread.h"
static pthread_key_t r1_key;
static pthread_once_t r1_once = PTHREAD_ONCE_INIT;
static void readline_destructor(void *ptr){
free(ptr);
}
static void readline_once(void){
pthread_key_create(&r1_key, readline_destructor);
}
typedef struct{
int r1_cnt;
char *r1_bufptr;
char r1_bbbuf[MAXLINE];
} Rline;
static ssize_t my_read(Rline *tsd, int fd, char *ptr){
if(tsd->r1_cnt <= 0){
again:
if((tsd->r1_cnt = read(fd, tsd->r1_buf, MAXLINE)) < 0){
if(errno == EINTR)
goto again;
return -1;
}else if(tsd->r1_cnt == 0)
return 0;
tsd->r1_bufptr = tsd->r1_buf;
}
tsd->r1_cnt--;
*ptr = *tsd->r1_bufptr++;
return 1;
}
ssize_t readline(int fd, void *vptr, size_t maxlen){
size_t n,rc;
char c,*ptr;
Rline *tsd;
pthread_once(&r1_once, readline_once);
if((tsd = pthread_getspecific(r1_key)) == NULL){
tsd = calloc(1, sizeof(Rline));
pthread_setspecific(r1_key, tsd);
}
ptr = vptr;
for(n=1;n<maxlen;n++){
if((rc = my_read(tsd, fd, &c)) == 1){
*ptr++ = c;
if(c == '\n')
break;
}else if(rc == 0){
*ptr = 0;
return n-1;
}else
return -1
}
*ptr = 0;
return n;
}
-
互斥锁
举例说明并发编程可能出现的错误:
- 线程A运行,把nconn的值(3)装载到一个寄存器;
- 系统把运行线程从A切换到B。A的寄存器被保存,B的寄存器则被回复;
- 线程B执行与C表达式nconn一一相对应的3条指令,把新值2存储到conn;
- 一段时间后,系统把运行线程从B切换回A。A的寄存器被回复,A从原来的地方(即3指令序列中的第二条指令)继续执行,把那个寄存器的值从3递减为2,再把2存储到conn;
- 最终结果是nconn把本该为1实际却为2;
【注】调用了fork,父进程和子进程除了描述符之外不共享任何东西。
对于访问共享变量可能出现的错误,解决办法如下:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
//返回:若成功则为0,若出错则为正的Exxx值
如果某个互斥锁把变量是静态分配的,我们就必须把它初始化为常值PTHREAD_MUTEX_INITIALIZER。如果我们在共享内存区中分配一个互斥锁,那么必须通过调用pthread_mutex_init函数在运行时把它初始化。
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
//使用时
pthread_mutex_lock(&counter_mutex);
...
pthread_mutex_unlock(&counter_mutex);
-
条件变量
互斥锁适合于防止同时访问某个共享变量,但是我们需要另外某种在等待某个条件发生期间让我们进入睡眠的东西。
- 互斥锁提供互斥机制,条件变量提供信号机制。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
//返回:若成功则为0,若出错则为正的Exxx值
使用示例:
int ndone;
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;
//通过在持有该互斥锁期间递增该计数器并发送信号到该条件变量
//一个线程通知主循环自身即将停止
pthread_mutex_lock(&ndone_mutex);
ndone++;
pthread_cond_signal(&ndone_cond);
pthread_mutex_unlock(&ndone_mutex);
//主循环阻塞在pthread_cond_wait调用中,等待某个即将终止
//的线程发送信号到与ndone关联的条件变量
while(nlefttoread > 0){
while(nconn < maxnconn && nlefttoconn > 0){
/*find a file to read*/
...
}
/*wait for one of the threads to terminate*/
pthread_mutex_lock(&ndone_mutex);
while(ndone == 0){
pthread_mutex_unlock(&ndone_mutex);
pthread_cond_wait(&ndone_cond, &ndone_mutex);
pthread_mutex_lock(&ndone_mutex);
}
for(i=0;i<nfiles;i++){
if(file[i].f_flags & F_DONE){
pthread_join(file[i].f_tid, (void **) &fptr);
/*update file[i] for terminated thread*/
...
}
}
}
上述“wait for one of the threads to terminate”处代码仍然存在问题:主线程外最后一个线程在主线程调用pthread_mutex_unlock和pthread_cond_wait之间终止并递增ndone的值。
pthread_cond_signal通常唤醒等待响应条件变量上的单个线程。有时候一个线程知道自己应该唤醒多个线程,这种情况下它可以调用pthread_cond_broadcast唤醒等在相应条件把变量上的所有线程。
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr,
const struct timespec *abstime);
//返回:若成功则为0,若出错则为正的Exxx值
【参数解析】
- abstime:绝对时间,即从1970年1月1日UTC时间以来的秒数和纳秒数,使用如下: