我们最后一个使用线程的并发服务器程序设计范式是在程序启动阶段创建一个线程池,然后只让主线程调用accept,并把每个客户连接套接字传递给池中某个线程。为此,我们引入了一个Thread结构:
pthread08.h
------------------------
typedef struct { pthread_t thread_tid; /* thread ID */ long thread_count; /* # connections handled */ } Thread; Thread *tptr; /* array of Thread structures; calloc'ed */
#define MAXNCLI 32 int clifd[MAXNCLI], iget, iput; pthread_mutex_t clifd_mutex; pthread_cond_t clifd_cond;
|
这里的clifd数组由主线程往中存入已接受的已连接套接字描述符,并由线程池中的可用线程从中取出一个以服务响应客户。iput是往数组里存入的下一个元素下标,iget是从数组里取出的下一个元素的下标。我们使用
互斥锁和条件变量来实现以上的想法。
serv08.c
---------------------------------
/* include serv08 */ #include "unpthread.h" #include "pthread08.h"
static int nthreads; pthread_mutex_t clifd_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t clifd_cond = PTHREAD_COND_INITIALIZER;
int main(int argc, char **argv) { int i, listenfd, connfd; void sig_int(int), thread_make(int); socklen_t addrlen, clilen; struct sockaddr *cliaddr;
if (argc == 3) listenfd = Tcp_listen(NULL, argv[1], &addrlen); else if (argc == 4) listenfd = Tcp_listen(argv[1], argv[2], &addrlen); else err_quit("usage: serv08 [ ] <#threads>"); cliaddr = Malloc(addrlen);
nthreads = atoi(argv[argc-1]);
tptr = Calloc(nthreads, sizeof(Thread));
iget = iput = 0;
/* 4create all the threads */ for (i = 0; i < nthreads; i++) thread_make(i); /* only main thread returns */
Signal(SIGINT, sig_int);
for ( ; ; ) { clilen = addrlen; connfd = Accept(listenfd, cliaddr, &clilen);
Pthread_mutex_lock(&clifd_mutex); clifd[iput] = connfd; if (++iput == MAXNCLI) iput = 0; if (iput == iget) err_quit("iput = iget = %d", iput); Pthread_cond_signal(&clifd_cond); Pthread_mutex_unlock(&clifd_mutex); } } /* end serv08 */
void sig_int(int signo) { int i; void pr_cpu_time(void);
pr_cpu_time();
for (i = 0; i < nthreads; i++) printf("thread %d, %ld connections\n", i, tptr[i].thread_count);
exit(0); }
|
主线程接受一个连接后将调用Pthread_cond_signal向条件变量发送信号,以便唤醒睡眠在其上的线程。
pthread08.c
------------------------------
#include "unpthread.h" #include "pthread08.h"
void thread_make(int i) { void *thread_main(void *);
Pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void *) i); return; /* main thread returns */ }
void * thread_main(void *arg) { int connfd; void web_child(int);
printf("thread %d starting\n", (int) arg); for ( ; ; ) { Pthread_mutex_lock(&clifd_mutex); while (iget == iput) Pthread_cond_wait(&clifd_cond, &clifd_mutex); connfd = clifd[iget]; /* connected socket to service */ if (++iget == MAXNCLI) iget = 0; Pthread_mutex_unlock(&clifd_mutex); tptr[(int) arg].thread_count++;
web_child(connfd); /* process request */ Close(connfd); } }
|
每一个线程都调用Pthread_cond_wait睡眠在clifd_cond条件变量上,主线程调用Pthread_cond_signal在所有可能线程中轮询唤醒其中一个。
$ ./serv08 173.26.100.162 12345 10
thread 0 starting
thread 1 starting
thread 2 starting
thread 3 starting
thread 4 starting
thread 5 starting
thread 6 starting
thread 7 starting
thread 8 starting
thread 9 starting
user time = 0.012, sys time = 0.096006
thread 0, 246 connections
thread 1, 250 connections
thread 2, 252 connections
thread 3, 250 connections
thread 4, 251 connections
thread 5, 249 connections
thread 6, 249 connections
thread 7, 252 connections
thread 8, 253 connections
thread 9, 248 connections
-----------------------------------------------------------------------------
客户/服务器程序设计范式总结
前面,介绍了集中服务器程序设计范式,并针对同一个web风格的客户程序分别运行了它们,也比较了它们花在执行进程控制上的CPU时间。
下面得出几点总结性意见:
1. 当系统负载较轻时,每来一个客户请求现场派生一个子进程为之服务的传统并发服务器程序模型就足够了。
2. 相比传统的每个客户fork一次的设计范式,预先派生一个进程池或线程池的做法能够把进程控制CPU时间降低10倍以上,而且编写这些范式的程序并不十分复杂,不过,在前面给的例子基础上需要增加:监视闲置子进程数,随所服务客户数动态的增加或减少这个数目。
3. 某些实现运行多个子进程或线程同时阻塞在一个accept调用中,而另一些实现却要求包绕accept调用安置某种类型的锁加以保护.文件锁或互斥锁都可以使用.
4. 让所有子进程或线程自行调用accept通常比让父进程或主线程独自调用accept并把描述符传递给子进程或线程来得简单而且快速。
5. 使用线程通常远快于使用进程.不过选择每个客户一个子进程还是一个线程取决于操作系统提供什么支持,还取决于为服务每个客户需要用到其他什么程序.
各个范式的程序源代码包见
<<客户/服务器程序设计范式---迭代服务器程序>>后附件
阅读(1356) | 评论(0) | 转发(0) |