概述
传统unix中,如果一个进程需要另一个实体完成某事,就会fork一个子进程然后让其执行。
但是fork调用存在着以下几个问题:
-
fork是昂贵的。fork要将父进程的内存映像复制到子进程,并在子进程复制所有描述符。即便现在使用了“写时复制”技术,fork依然是昂贵的
-
fork返回之后,父子进程之间的通信需要进程间通信(IPC)。调用fork之前父进程向子进程传递信息是容易的,但是子进程向父进程返回信息确很困难
线程有助于解决这些问题,因为
- 线程创建成本较为低廉,比进程的创建可能快10-100倍
- 线程共享相同的全局内存,使得线程之间易于共享信息
基本线程函数:创建和终止
pthread_create函数
当程序由exec启动执行时候,称为初始线程或者主线程的单个线程就创建了。其他都用pthread_create函数创建。
int pthread_create(pthread_t *tid,
const pthread_attr_t * attr,
void *(*func)(void * ),
void * arg);
当线程创建成功的时候tid返回线程ID(pthraed_t);我们可以通过attr设置诸如线程优先级、初始栈大小、是否是守护线程等,一般可以用NULL表示使用默认值;func表示要该线程执行的函数,线程会调用该函数,然后通过显式的调用pthread_exit或者隐式的终止(通过return),函数的调用参数由arg指定,如果要传入多个数值,需要封装成struct然后再传入。
线程如果创建成功,函数返回0,否则返回正值表示具体错误(而不是返回-1并且设置errno)
pthread_join函数
我们可以用join来等待一个给定线程的终止,类似于waitpid。为此我们需要制定等待线程的tid(而不能等待任意一个线程结束)
int pthread_join(pthread_t *tid, void ** status)
如果status非空,那么所等待线程的返回值就会存入status指定的位置
pthread_self函数
线程可以调用该函数获知自己的tid(类似于进程中的getpid)
pthread_t pthread_self(void);
pthread_detach函数
一个线程要么是可汇合的(joinable,默认值),要么是脱离的(detached),一个可汇合的线程结束时候,线程ID和退出状态会留存到另一个线程对它调用pthread_join。脱离的线程终止的时候,所有相关的资源都被释放,不能等待终止。
int pthread_detach(pthread_t);
本函数一般是由想让自己脱离的线程调用,如以下语句
pthread_detach(pthread_self());
pthread_exit函数
让一个线程终止的方法之一就是调用pthread_exit函数
void pthread_exit(void * status);
如果该线程未曾脱离,其线程ID和退出状态会一直留存到调用进程内的某个其他线程对它调用pthread_join。
status不能指向局部于调用线程的对象,因为线程终止时候对象也消失。
线程终止的其他方法有:
- 启动线程的函数返回。其返回值就是线程的终止状态
- main返回或者任何线程调用了exit,进程终止,所有线程也随之终止。
使用线程的str_cli函数
#include "../unp.h"
#include <pthread.h>
void *copyto(void *);
static int sockfd;
static FILE *fp;
void str_cli(FILE *fp_arg, int sockfd_arg)
{
char recvline[MAXLINE];
pthread_t tid;
sockfd = sockfd_arg;
fp = fp_arg;
pthread_create(&tid, NULL, copyto, NULL);
}
void *copyto(void *arg)
{
char sendline[MAXLINE];
while (Fgets(sendline, MAXLINE, fp) != NULL)
Writen(sockfd, sendline, strlen(sendline));
Shutdown(sockfd, SHUT_WR);
return (NULL);
}
使用线程的TCP回射服务器程序
#include "../unp.h"
#include <pthread.h>
static void *doit(void *);
int main(int argc, char **argv)
{
int listenfd, connfd;
pthread_t tid;
socklen_t addrlen, len;
struct sockaddr *cliaddr;
if (argc == 2)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
else
err_quit("usage: tcpserv01 [ <host> ] <service or port>");
cliaddr = Malloc(addrlen);
for (;;)
{
len = addrlen;
connfd = Accept(listenfd, cliaddr, &len);
pthread_create(&tid, NULL, &doit, (void *)connfd);//这里直接就是一个连接一个线程
}
}
static void *doit(void *arg)
{
pthread_detach(pthread_self());
str_echo((int)arg);
Close((int)arg);
return NULL;
}
注意在上述调用pthread_create中我们传入了整数类型connfd,ANSI C并不能保证这样做有用,只有在指针大小和整数类型大小一致的系统上才能起作用,不过绝大多数UNIX都符合这个特征。
那么问题来了?为什么我们不直接传入connfd地址呢?答案是不同的线程因此会共享该地址,从而互相干扰。
除此之外我们可以提供一个更好的方法
#include "../unp.h"
#include <pthread.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)
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
else if (argc == 3)
listenfd = 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;
}
注意二者的差异,此时我们通过调用了malloc来分配空间,从而避免互相干扰
线程安全函数
在上面我们使用到了malloc和free函数,这两个函数是不可重入的。这意味着当主线程处于函数内部执行流程的时候,信号处理函数如果调用这两个函数之一,会可能造成严重后果。那么我们怎么可以调用这两个函数呢?posix规定这两个函数和其他很多函数必须是线程安全的。
不必线程安全的版本 | 必须线程安全的版本 | 注释 |
---|---|---|
asctime | asctime_r | |
ctermid | 参数非空时线程安全 | |
ctime | ctime_r | |
getc_unlocked | ||
getchar_unlocked | ||
getgrid | getgrid_r | |
getgrnam | getgrnam_r | |
getlogin | getlogin_r | |
getpwnam | getpwnam_r | |
getpwuid | getpwuid_r | |
gmtime | gmtime_r | |
localtime | localtime_r | |
putc_unlocked | ||
putchar_unlocked | ||
rand | rand_r | |
readdir | readdir_r | |
strtok | strtok_r | |
tmpnam | 参数非空时线程安全 | |
ttyname | ttyname_r | |
gethostXXX | ||
getnetXXX | ||
getprotoXXX | ||
getservXXX | ||
inet_ntoa |
期中ctermid和tmpnam线程安全条件是,调用者为返回结果预先分配空间,并把指向该控件的指针作为参数传递给函数。
线程特定数据
当我们使用线程的时候,有时候会碰到因为有函数使用静态变量而引起的错误,因为静态变量无法为不同的线程保存各自的值。对此的解决办法有以下几个:
-
使用线程特定数据。优点在于调用顺序不用变动,所有变动都体现在库函数而不是应用程序。缺点在于较为复杂,而且转换成了只能在支持现成的系统上工作的函数
-
改变调用顺序,如调用者吧readline的所有调用参数封装在一个结构,并在该结构中存入该静态变量
typedef struct{ int read_fd; char *read_ptr; size_t read_maxlen; int rl_cnt; char *rl_bufptr; char r1_buf[MAXLINE]; } void readline_rinit(int ,void *,size_t,Rline*); ssize_t readline_r(Rline *); ssizse_t Readline_r(Rline*);
这样的话无论是否支持线程,函数都可以使用。但是要修改所有用到相关函数的应用程序
-
改变接口结构,避免使用静态变量。sss
我们不妨先试图使用线程特定数据。每个系统都支持优先数量的线程特定数据元素(POSIX要求不小于128/进程)。系统为每个进程维护一个我们称之为Key结构的结构数组,每个Key包含有标志和析构函数指针。
Key中的标志指示该数组元素是否正在使用,所有的标志都初始化为不在使用。当线程调用pthread_key_create创建一个新的线程特定数据元素时候,系统会搜索Key结构数组找到第一个不在使用的元素,然后返回数组索引。
除了进程范围的Key结构数组外,系统还会在进程内维护每个线程的多条消息。这些特定于线程的信息我们称之为Pthread结构,其部分内容是我们称之为pkey数组的一个128元素的指针数组,pkey数组的所有元素都被初始化为空指针,这些指针是和进程内的128个可能的“键”相关联的。
可能比较混淆,实际上来说,我们可以认为每个线程都自带有一定的线程特定数据元素数组,然后函数可以声明得到一个key数组中的index,拿着这个index去初始化每个线程中pkey[index],从而确保每个线程都有自己的空间。
一个使用线程特定数据来维护线程各自状态的readline函数的运行逻辑会像下面一样:
- 进程被启动,多个线程被创建
- 其中一个线程(假设是0)是首个调用readline函数的线程,readline函数会调用pthread_key_create获取一个未用的元素,假定是1(readline用pthread_once函数确保pthread_key_create只会被调用一次)
- readline调用pthread_getspecific获取本线程的pkey[1]值,返回的会是一个空指针。readline于是调用malloc分配内存去,初始化内存区,然后调用pthread_setspecific设置pkey[1]为刚分配的内存区
- 另一个线程(假定是N)调用了readline,同样试图调用pthread_once初始化键,不过因为线程0已经调用过了,所以会不再被调用
- readline调用pthread_getspecific发现同样为空指针,于是同样用malloc初始化
当线程终止时候,我们在调用pthread_key_create时候传入的析构函数将会启动,从而让我们有机会释放内存
一个典型的关于pthread_key_create和pthread_once使用的例子大概如下图
pthread_key_t r1_key;
pthread_once_t r1_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函数
#include "../unp.h"
#include <pthread.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_buf[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));
}
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;
}
Web客户与同时连接
我们可以用线程代替16章中的非阻塞connect,允许让他们使用默认的阻塞模式。
#include "../unp.h"
#include <pthread.h>
#define MAXFILES 20
#define SERV "80"
struct file
{
char *f_name;
char *f_host;
int f_fd;
int f_flags;
pthread_t f_tid;
} file[MAXFILES];
#define F_CONNECTING 1
#define F_READING 2
#define F_DONE 4
#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"
int nconn, nfiles, nlefttoconn, nlefttoread;
void *do_get_read(void *);
void home_page(const char *, const char *);
void write_get_cmd(struct file *);
int main(int argc, char **argv)
{
int i, n, maxnconn;
pthread_t tid;
struct file *fptr;
if (argc < 5)
err_quit("usage: web <#conns> <IPaddr> <homepage> file1 ...");
maxnconn = atoi(argv[1]);
nfiles = min(argv - 4, MAXFILES);
for (i = 0; i < nfiles; i++)
{
file[i].f_name = argv[i + 4];
file[i].f_host = argv[2];
file[i].f_flags = 0;
}
printf("nfiles = %d \n", nfiles);
home_page(argv[2], argv[3]);
nlefttoread = nlefttoconn = nfiles;
nconn = 0;
while (nlefttoread > 0)
{
while (nconn < maxnconn && nlefttoconn > 0)
{
for (i = 0; i < nfiles; i++)
if (file[i].f_flags == 0)
break;
if (i == nfiles)
err_quit("nlefttoconn= %d but nothing found", nlefttoconn);
file[i].f_flags = F_CONNECTING;
pthread_create(&tid, NULL, &do_get_read, &file[i]);
file[i].f_tid = tid;
nconn++;
nlefttoconn--;
}
//注意thr_join是Solaris下的<thread.h>,其作用类似于waitpid
//可以等待任意一个线程结束,但是posix并没有该函数(所以该函数不能跑)
if ((n = thr_join(0,&tid, (void **)&fptr)) != 0)
errno = n, err_sys("thread join error");
nconn--;
nlefttoread--;
printf("thread id %d for %s done\n", tid, fptr->f_name);
}
exit(0);
}
void *do_get_read(void *vptr)
{
int fd, n;
char line[MAXLINE];
struct file *fptr;
fptr = (struct file *)vptr;
fd = Tcp_connect(fptr->f_host, SERV);
fptr->f_fd = fd;
printf("do_get_read for %s,fd %d,thread %d\n", fptr->f_name, fd, fptr->f_tid);
write_get_cmd(fptr);
for(;;){
if ( (n=Read(fd,line,MAXLINE))==0)
break;
printf("readk %d bytes from %s\n",n,fptr->f_name);
}
printf("end-of-file on %s\n",fptr->f_name);
Close(fd);
fptr->f_flags=F_DONE;
return(fptr);
}
互斥锁
在线程中我们可以使用互斥锁来保护共享变量,这使用到了pthread_mutex_t和pthread_mutex_lock以及pthread_mutex_unlock
int pthread_mutex_lock(pthread_mutex_t *);
int pthread_mutex_unlock(pthread_mutex_t *);
如果该变量是静态分配的,那么我们就要初始化为常值PTHREAD_MUTEX_INITIALIZER,如果我们在共享内存中分配一个互斥锁,那么就要调用pthread_mutex_init函数在运行时初始化
pthread_mutext_t counter_mutex=PTHEAD_MUTEX_INITIALIZER;
int main(int argv,char ** argv){
...
pthread_mutex_lock(&counter_mutex);
...
pthread_mutex_unlock(&counter_mutex);
}
条件变量
unix同样提供了API来实现条件变量
int pthread_cond_wait(pthread_cond_t * cptr,
pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *);
一个常见的使用模式是
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_mmutex);
pthread_mutex_lock(&ndone_mutex);
while(ndone==0)
pthread_cond_wait(&ndone_cond,&ndone_mutex);
...
pthread_mutex_unlock(&ndone_mutex);
除此之外,我们如果需要唤醒多个线程或者有时间限制的等待,我们可以用下面的函数
int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(
pthread_cond_t * cptr, pthread_mutex_t * mptr,
const struct timespec * abstime);
pthread_cond_timewait允许线程设置一个阻塞事件的限制,如果发生了超时,就会设置ETIME错误,abstime设置的是绝对时间,从1970年1月1日你UTC时间以来的秒数和纳秒数,通常调用过程是调用gettimeofday获取当前时间的timeval结构,复制到timespec结构中,再加上自己期望的时间限制。优点在于如果该函数过早返回(可能因为捕获了信号),那么不必改动timespec结构参数就可以再次调用该函数
struct timeval tv;
struct timespec ts;
if (gettimeofday(&tv,NULL)<0)
err_sys("gettimeofday error");
ts.tv_sec=tv.tv_sec+5;//设置为5s之后
ts.tv_nsec=tv.tv_usec*1000//从微秒到纳秒
pthread_cond_timedwait(...,&ts);
Web客户与同时连接(续)
在之前我们书写的版本中,我们使用到了solaris的thr_join从而失去了可移植性,因此我们可以对此做出一些更改。
#include "../unp.h"
#include <pthread.h>
#define MAXFILES 20
#define SERV "80"
struct file
{
char *f_name;
char *f_host;
int f_fd;
int f_flags;
pthread_t f_tid;
} file[MAXFILES];
#define F_CONNECTING 1
#define F_READING 2
#define F_DONE 4
#define F_JOINED 8
#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"
int nconn, nfiles, nlefttoconn, nlefttoread;
int ndone;
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;
void *do_get_read(void *);
void home_page(const char *, const char *);
void write_get_cmd(struct file *);
int main(int argc, char **argv)
{
int i, n, maxnconn;
pthread_t tid;
struct file *fptr;
if (argc < 5)
err_quit("usage: web <#conns> <IPaddr> <homepage> file1 ...");
maxnconn = atoi(argv[1]);
nfiles = min(argv - 4, MAXFILES);
for (i = 0; i < nfiles; i++)
{
file[i].f_name = argv[i + 4];
file[i].f_host = argv[2];
file[i].f_flags = 0;
}
printf("nfiles = %d \n", nfiles);
home_page(argv[2], argv[3]);
nlefttoread = nlefttoconn = nfiles;
nconn = 0;
while (nlefttoread > 0)
{
while (nconn < maxnconn && nlefttoconn > 0)
{
for (i = 0; i < nfiles; i++)
if (file[i].f_flags == 0)
break;
if (i == nfiles)
err_quit("nlefttoconn= %d but nothing found", nlefttoconn);
file[i].f_flags = F_CONNECTING;
pthread_create(&tid, NULL, &do_get_read, &file[i]);
file[i].f_tid = tid;
nconn++;
nlefttoconn--;
}
//这里利用增加条件变量的办法,如果发现有线程完成(线程会调用signal通知),就轮询查找
//哪个线程是完成的
pthread_mutex_lock(&ndone_mutex);
while (ndone == 0)
pthread_cond_wait(&ndone_cond, &ndone_mutex);
for (i = 0; i < nfiles; i++)
{
if (file[i].f_flags && F_DONE)
{
pthread_join(file[i].f_tid, (void **)&fptr);
if (&file[i] != fptr)
err_quit("file[i] != fptr");
fptr->f_flags = F_JOINED;
nconn--;
nlefttoread--;
ndone--;
printf("thread id %d for %s done\n", tid, fptr->f_name);
}
}
pthread_mutex_unlock(&ndone_mutex);
}
exit(0);
}
void *do_get_read(void *vptr)
{
int fd, n;
char line[MAXLINE];
struct file *fptr;
fptr = (struct file *)vptr;
fd = Tcp_connect(fptr->f_host, SERV);
fptr->f_fd = fd;
printf("do_get_read for %s,fd %d,thread %d\n", fptr->f_name, fd, fptr->f_tid);
write_get_cmd(fptr);
for (;;)
{
if ((n = Read(fd, line, MAXLINE)) == 0)
break;
printf("readk %d bytes from %s\n", n, fptr->f_name);
}
printf("end-of-file on %s\n", fptr->f_name);
Close(fd);
pthread_mutex_lock((&ndone_mutex));
fptr->f_flags = F_DONE;
ndone++;
pthread_cond_signal(&ndone_cond);
pthread_mutex_unlock(&ndone_mutex);
return (fptr);
}