《第6章-网络服务器的设计模式(共24页).doc》由会员分享,可在线阅读,更多相关《第6章-网络服务器的设计模式(共24页).doc(24页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、精选优质文档-倾情为你奉上第6章 网络服务器的设计模式6-1 概 述第3章和第4章中讨论的是最基本的TCP服务器和UDP服务器:串行单用户服务器,本章将讨论服务多客户的网络服务器的设计模式。因为大多数网络服务器都是多进程或多线程应用,因此在讨论网络服务器的设计模式之前先介绍进程和线程的基本概念、编程接口以及程序设计方法。6-2 多进程环境下的网络编程6-2-1 进程的基本概念在传统的多任务系统中,“进程”是一个很重要的概念,在系统中进程是程序执行的实体,它由一组机器指令(也称为“正文”)、数据以及堆栈组成。当一个进程需要由另一个实体执行某项任务时,该进程就派生(fork)一个子进程,让子进程去
2、进行处理。当一个程序按多任务组织时,在多任务操作系统核心的调度下,系统中许多进程在并发执行,这些进程是同一程序的不同体现。每个进程按自己的一系列指令严格执行,各进程之间的指令空间是独立的,不用跳转到另外的进程去,它只会读写自己的数据与堆栈,不能去读写另外进程的数据和堆栈。进程通过系统调用与其他进程和外界通信。采用多进程的方式来实现处理多任务的程序主要有以下不足之处:(1)用fork()创建子进程代价比较高。操作系统需要将父进程的内存映像拷贝到子进程,同时还要复制所有打开的描述符等数据。 (2)父进程与子进程之间的信息交换需要用到进程间通信(IPC),如共享内存(Shared Memory),报
3、文队列(Message Queue),管道(Pipe),甚至网络通信等。这种通信的代价要比同一地址空间内的变量共享高得多。采用线程可以解决上面的问题。6-2-2 多进程下的网络编程Unix系统中派生新进程的唯一的方法是调用fork()函数。该函数的原型如下:#include pid_t fork(void);fork函数的一个最重要的特性是:调用一次,如果调用成功则返回两次:在调用进程(父进程)中,它返回一次,返回值为新派生的子进程的进程ID号,父进程需要通过它来区别不同的子进程;在新派生的子进程中还返回一次,返回值为0,子进程可以通过getpid获取父进程的进程ID。如果函数调用失败,则只在
4、调用进程中返回-1。fork函数的两个典型应用是:(1)一个进程在处理一个操作时,同时还需要处理另一个操作,这时,进程就派生一个子进程来执行该任务。大多数网络服务器就是这种应用的典型代表。(2)一个进程在执行过程中,想执行另一个程序,它就通过fork产生一个自己的副本(子进程)。再由子进程调用exec序列函数来代替自己去执行新程序。shell程序中常采用这种方式。在网络服务器中,最常见的使用fork函数的工作方式是:父进程调用accept,当客户连接到达,accept成功返回时,调用fork派生一个子进程。然后,父进程关闭accept返回的已连接描述符,继续在监听描述符上调用accept等待下
5、一个连接请求。而子进程则关闭父进程使用的监听描述符,使用accept返回的已连接描述符进行读写操作。上述过程可以用程序6-1来表示。程序6-1 典型的调用fork的网络服务器程序结构。 /* 其他变量定义 */int sockfd, connfd;pid_t pid;sockfd = socket(.); /* 创建一个新的插口 */* 设置服务器地址结构变量代码 */bind(sockfd, .); /* 将服务器插口绑定到本地地址和端口 */listen(sockfd, n); for (; ;) connfd = accept(sockfd, .); /* 阻塞等待 */if (pid=
6、 fork() = 0) /* if中的代码只在子进程中的执行 */ close(sockfd); /* 子进程关闭父进程使用的监听插口描述符 */ /* 通过connfd读写操作,任务处理 */ close(connfd); exit(0); if (pid = -1) prinf(调用fork()派生子进程失败 !n);close(connfd); /* 在父进程中关闭子进程使用的已连接描述符 */下面我们来讨论为什么要在父进程中关闭已连接描述符,而在子进程中要关闭监听描述符,以及这样的做法不会造成不良后果的原因。当一个描述符正被某一进程使用,又被另一进程打开时,并不是真的创建新的内存I结
7、点,而只是将该描述符的引用数加1。调用fork()派生的子进程将继承父进程的所有打开的描述符(相当于子进程中又打开了一次),所以成功调用fork()后,父进程打开的所有描述符的引用数都将加1。在程序6-1中,父进程调用close关闭子进程使用的已连接插口时,它只是将描述符connfd的访问计数值减1。由于connfd的访问计数值仍然大于0,所以父进程中的这个close调用并没有使得内核启动TCP的四分组连接终止序列。因而不会影响子进程中对connfd的读写操作。同样子进程中的close调用也没有关闭父进程使用的监听描述符。如果父进程不关闭accept返回的已连接插口,将会发生下列严重情况:(1
8、)父进程最终将用尽其可用描述符,因为任何进程中允许同时打开的描述符的数量是有限的。(2)没有一个客户连接被终止。这是因为,当子进程完成任务后调用close关闭其使用的已连接插口时,它的访问计数值只是由2变为1,连接并没有被关闭,资源仍被占用未释放。如果子进程中不关闭父进程使用的监听描述符,则父进程调用close关闭监听描述符时也并没有真正关闭它,资源得不到释放。在多进程应用中,如果一个进程确实要终止一个连接,但这个连接所对应的插口又被其他进程所使用,则必须通过shutdown而不是close来终止连接。关于shutdown的使用在第2章中已经详细讨论过。在程序6-1中,当子进程处理完所有任务时
9、,它调用exit()退出。但是,子进程中的exit()只能关闭所有打开的描述符以及释放其他的一些系统资源,对于系统核心中的进程控制块(PCB)表中的存放的进程信息(进程的ID、终止状态,以及子进程的资源利用信息等)却得不到释放。这时,子进程将进入僵尸状态(Zombie),成为系统中的僵尸进程。设置僵尸状态的目的是为了方便父进程处理子进程结束后的状态。但是,僵尸进程占用内核空间,极端情况下将导致系统无法正常工作。因此,父进程必须调用wait()函数,清除自己创建的子进程。wait()函数的功能是让父进程获取子进程的结束状态信息并释放子进程在PCB表中的资源。其函数原型如下:#include pi
10、d_t wait(int *stat)wait的返回值为子进程的进程号,传址参数stat被用来返回子进程退出时的状态值(exit()调用中的传入参数)。如果子进程已退出,wait将把结束的子进程完全释放,并获得其返回值。但是,如果子进程的执行没有结束,wait将阻塞调用它的父进程。解决办法是在信号SIGCHLD中断处理子程序中调用wait或waitpid。因为如果有子进程被中断,内核将给父进程发送SIGCHLD信号。下面我们来看看调用wait函数的SIGCHLD信号中断处理子程序的程序结构,如程序6-2所示。程序6-2 调用wait的SIGCHLD信号中断处理子程序。void sigchld_
11、handler(int sig) pid_t pid;int stat;pid = wait(&stat);/* 处理stat的代码 */return; 但是,上述中断处理子程序还存在着产生僵尸进程的可能性。假定一个网络服务器同时为多个客户服务(服务器为每一个客户派生一个服务子进程),如果在某一时刻,有多个客户同时关闭连接,将导致各自的服务子进程几乎同时终止。这些服务子进程的父进程也将几乎同时收到多个SIGCHLD信号,但是Unix信号一般是不排队的,并且系统能处理多少个几乎同时到达的信号是未知的,可能是一个,也可能来得及处理两个或更多。正确的解决办法是调用Unix提供的另一个系统调用wait
12、pid()而不是wait()。waitpid的函数原型如下:#include pid_t waitpid(pid_t pid, int *stat, int options);第一个参数pid指定所等待的子进程的进程ID,如果将其设为 -1,则表示第一个终止的子进程。第二个参数stat返回终止进程的退出状态,与wait函数中的一样。第三个参数options指定函数waitpid等待的方式。宏定义WNOHANG表示若无子进程结束就返回。如果返回0,表示没有子进程终止,如果大于0则表示终止的子进程的进程ID。修改后的SIGCHLD信号的中断处理子程序如程序6-3所示。程序6-3 调用waitpid
13、的SIGCHLD信号中断处理子程序。void sigchld_handler(int sig) pid_t pid;int stat;while (pid = waitpid(-1, &stat, WNOHANG) 0) /* 处理stat的代码 */ 程序6-3将清除所有已经结束的子进程的残余部分。因此,为了不产生僵尸进程,在多进程应用的主进程中一般要捕获SIGCHLD信号(加入语句signal(SIGCHLD, sigchld_handler),信号处理子程序如程序6-3所示。如果一个进程终止,且该进程有子进程处于僵尸状态,则所有僵尸子进程的父进程ID均置为1(关机进程init)。init
14、进程将作为这些僵尸进程的父进程负责清除它们。总之,在多进程网络编程中,为了程序正确运行,要做到以下几点:(1)在子进程中关闭父进程使用的插口,在父进程中关闭子进程使用的插口。(2)在主进程中捕获SIGCHLD信号,在信号的中断处理子程序中调用waitpid函数完全释放服务子进程占用的资源。6-3 多线程环境下的应用程序设计6-3-1 线程的基本概念线程(Thread),有时也称为轻进程(Lightweight Process),是程序中被执行的指令序列,它具备有程序计数器(PC)与保持局部变量与返回地址痕迹的堆栈等最少状态参数来控制它的执行。线程是具有最少开销的程序实体,具有以下属性:(1)线
15、程是任务中程序流的控制点。(2)任务中的线程可以访问该任务的所有资源,这些资源包括:进程指令,大多数数据(全局数据),信号处理程序,工作环境信息(当前工作目录、用户ID和组ID等)。同时,每个线程有自己的一些专用资源:寄存器组(包括程序计数器和栈指针),线程堆栈(存放线程自己的局部变量和返回地址),优先级,线程ID,信号屏蔽掩码,全局错误代码errno。(3 一个线程可以与其他线程并行执行,甚至在一个任务中的所有线程都可并行执行。(4)线程是具有最少状态的轻型实体,它的开销最低。一般来说,创建线程要比创建进程快10100倍。并且线程之间的切换是在同一地址空间内进行的, 因此切换开销也比较小。在
16、传统的多任务操作系统中, 进程(process)是一个重要的概念。在系统中, 进程是程序执行的实体, 它由一组机器指令、数据和堆栈组成。各进程的指令是独立的,它只会读写自己的数据与堆栈而不能读写其他进程的数据与堆栈。进程通过系统调用与其他进程以及外界通信。有了线程的概念后,程序的执行单位就由传统的进程变为线程了,而进程则是作为资源的分配单位存在。同时,同一进程内的不同线程之间的通信也变得简单了, 因为它们都共享同一地址空间内的所有资源。总之,多线程有以下优点:改善应用响应时间, 提高多处理系统的效率, 改善程序结构, 开销比较小, 提高性能。一般来说,根据线程所处的级别,可以将线程分为三种:用
17、户级线程,内核级线程和混合型线程。用户级线程通常运行在一个已有的操作系统之上,进程中的线程对内核是不可见的。用户级线程之间将竞争分配给进程的资源。这些线程是通过运行时间(Runtime)系统即进程的部分代码来调度的。使用用户级线程包的程序通常被链接到一个特殊的库中,在此库中的每一个库函数和系统调用被封装在一个外壳中,这个外壳代码能调用运行时间系统来执行线程管理。用户级线程的优点是开销特别低,但它也有一些缺点:CPU受限的线程几乎不执行系统调用或库调用;因为线程仅共享分配给进程的处理机资源,所以限制了可并行的线程的数量。在内核级线程中,内核将每个线程看作一个可调度的实体。线程在一个广阔的基础上竞
18、争处理机资源。内核级线程的调度如同调度进程自身一样耗时,但内核级线程可以利用多处理机。内核级线程提供的同步和共享数据的耗时不比进程的长,但与用户级线程相比,却要长得多。混合型线程具有用户级线程和内核级线程的优点。它提供两级控制:用户依照用户级线程来写程序,然后指定与进程相连的内核可调度的实体的数量。当进程正在运行以获得并行性时,用户级线程被映射到内核可调度的实体上。具体的映射方法依赖于实现。pthread线程调度模型就是一种混合模型。尽管有不同的线程机制,但是对编程人员而言,这三种方法都是透明的,可以置之不理,只需知道系统提供了哪些编程接口就可以了。6-3-2 线程的基本编程接口本章我们介绍P
19、osix线程,因为现有的大多数实现均支持Posix线程。Posix线程接口也称为pthread接口,所有接口函数均以pthread_为前缀。除了流行的pthread接口外,还有Solaris 2上的thread接口(Solaris 2.5以及更高的版本既支持Solaris 2线程,又支持pthread线程),SunOS中的LWP接口等。而且,不同实现上的pthread接口提供的函数的个数也可能有些差别,大多数实现不同程度上增强了Posix标准中定义的pthread函数接口。本章只介绍常用的pthread接口,如表6-1所示。表6-1 pthread接口中的函数类 型函 数 名功 能创建与终止p
20、thread_create创建一个新的线程,类似于进程接口中的forkpthread_join等待一个线程终止,类似于进程接口中的waitpidpthread_self返回线程ID,类似于进程接口中的getpidpthread_detach将指定的线程变为脱离的(detached),如果线程是脱离的,则当它终止时,所有的资源都将立即释放,别的线程不能等待它终止。该函数常被想脱离自己的线程调用 (即:pthread_detach(pthread_self();)pthread_cancel请求指定线程终止运行,被请求终止的线程何时终止是不可知的pthread_exit终止线程。还有另外三种终止线
21、程的方法:线程主函数(pthread_create函数中的第三个参数)返回;另一个线程调用pthread_cancel请示线程终止运行;进程的main函数返回或任何线程调用了exit,所有线程将随进程的终止而终止特定数据处理pthread_once调用初始化子程序,该子程序只能被一个线程执行一次pthread_keycreate创建一个唯一的每线程上下文中的键值,对于一个进程内的指定键,该函数只能被调用一次加锁解锁函数pthread_mutex_lock给一个未加锁的锁加锁。如果试图为一个已被其他线程锁住的互斥锁加锁,程序将会阻塞直到该互斥锁被解锁pthread_mutex_unlock给一个
22、加锁的锁解锁pthread_mutex_trylock加锁,但是如果其他线程已加锁,则调用线程不等待而是立即返回条件变量函数pthread_cond_wait等待一个条件变量被唤醒pthread_cond_signal唤醒等待指定条件变量的线程pthread_cond_timewait在指定时间间隔内等待一个条件变量被唤醒pthread_cond_broadcast唤醒所有等待在指定条件变量上的线程pthread_cond_destroy删除一个条件变量pthread_cond_init创建一个条件变量线程属性函数pthread_attr_create创建线程属性对象。线程属性包括:优先级,堆
23、栈大小,调度级,继承的调度级等pthread_attr_delete删除线程属性对象表6-2 列出了表6-1中所有函数的原型及参数说明。调用pthread编程接口必须在程序中包括线程定义头文件。函数pthread_exit()没有返回值;函数pthread_self()的成功返回值为线程自己的ID;函数pthread_mutex_trylock调用成功时返回1,如果已经加锁则返回0,传入参数值错误则返回-1。对于其他的函数,成功时返回0,失败则返回-1,错误代码保存在errno中。表6-2 pthread函数接口原型函数原型参数说明int pthread_create(pthread_t *t
24、hread, const pthread_attr_t *attr, void *(*start_func)(void ), void *arg);thread:线程ID,attr:指定线程属性,start_func: 线程主函数,arg:线程主函数的传入参数int pthread_join(pthread_t thread, void *status);thread:等待线程的ID,status:返回结束线程的返回值int pthread_self(void)无参数int pthread_detach(pthread_t thread)thread:请求脱离的线程IDint pthread_
25、cancel(pthread_t thread);thread:请求终止的线程IDvoid pthread_exit(void *status);status:线程退出状态,它不能指向局部于调用线程的对象int pthread_once(pthread_once_t *once, void (*init)(void);once指向的值确保init函数只被调用一次int pthread_keycreate(pthread_key_t *key, void ( *destructor ) (void *value);当线程终止时,调用destructor删除key指定的键值int pthread_
26、mutex_lock(pthread_mutex_t *mutex)mutex:锁int pthread_mutex_unlock(pthread_mutex_t *mutex);mutex:锁int pthread_mutex_trylock(pthread_mutex_t *mutex);mutex:锁int pthread_cond_wait(phtread_cond_t *cond, pthread_mutex_t *mutex);cond:条件变量, mutex:条件变量锁int pthread_cond_signal(pthread_cond_t *cond);cond:条件变量i
27、nt pthread_cond_timewait( phtread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *abstime);cond:条件变量, mutex:条件变量锁,abstime:超时值int pthread_cond_broadcast(pthread_cond_t *cond);cond:条件变量int pthread_cond_destroy(pthread_cond_t *cond);cond:条件变量int pthread_cond_init(pthread_cond_t *cond, pthread_co
28、ndattr_t *attr);cond:条件变量, attr:条件变量属性int pthread_attr_create(pthread_attr_t *attr);attr:线程属性变量int pthread_attr_delete(pthread_attr_t *attr);attr:线程属性变量表6-3 列出了Solaris 2线程接口,并给出了对应的Posix的pthread接口。表6-3 Posix线程接口与Solaris 2 线程接口的比较Posix线程接口Solaris 2线程接口pthread_createthr_createpthread_exitthr_exitpthre
29、ad_killthr_killpthread_jointhr_joinpthread_selfthr_selfpthread_mutex_initmutex_initpthread_mutex_destroymutex_destroypthread_mutex_lockmutex_lockpthread_mutex_trylockmutex_trylockpthread_mutex_unlockmutex_unlockpthread_cond_initcond_initpthread_cond_destroycond_destroypthread_cond_waitcond_waitpthre
30、ad_cond_timewaitcond_timewaitpthread_cond_signalcond_signalpthread_cond_broadcastcond_broadcast6-3-3 多线程程序设计本节讨论基本的多线程程序设计,包括线程的创建和终止,线程互斥和同步,线程属性设置等内容。1线程的创建和终止程序6-4显示如何创建线程以及如何向线程主函数传递参数。例子中,主线程一共创建了5个线程,每个线程在等待10秒钟后退出。主线程在创建所有线程后等待它们结束后退出。程序6-4 一个简单的多线程示例程序thread_test.c。1 #define _REENTRANT 2 #in
31、clude 34 #define NUM_THREADS 55 #define SLEEP_TIME 1067 void *sleeping(void *); 8 int i;9 pthread_t tidNUM_THREADS; 1011 main( int argc, char *argv ) 12 13 for ( i = 0; i NUM_THREADS; i+) 14 pthread_create(&tidi, NULL, sleeping, SLEEP_TIME); 15 for ( i = 0; i NUM_THREADS; i+) 16 pthread_join(tidi, N
32、ULL); 17 printf(all %d threads have terminatedn, i); 18 /* main */ 19 20 void *sleeping(void *arg ) 21 22 int sleep_time = (int)arg; 23 printf(thread %d sleeping %d secondsn, 24 pthread_self(), sleep_time); 25 sleep(sleep_time); 26 printf(nthread %d awakeningn, pthread_self(); 27 第12行 几乎所有的多线程程序都有这两
33、行。第1行定义宏变量_REENTRANT,指示编译器使用可重入的函数库。该宏定义也可以在编译时用编译选项-D_REENTRANT来定义。第2行包括线程定义头文件pthread.h。第1314行 主线程产生NUM_THREADS个线程。第1516行 主线程等待所有它产生的线程终止。实际应用中,主线程常常在处理完自己的工作后,才去等待其他线程的终止。第2027行 主线程创建的线程的主函数定义。第24行调用sleep()睡眠,一个线程调用sleep()不会影响到另一个线程,即只有调用sleep()的线程才会睡眠,下面的运行结果也显示了这一点。编译并运行程序6-4,结果将显示:差不多经过10秒钟后,进
34、程即退出,即使在单处理器的主机上。但是,如果将程序6-4改为单线程进程运行(主进程连续调用sleeping() NUM_THREADS次后退出),则程序大约运行50秒(NUM_THREDS * SLEEP_TIME)后才退出。以此可以看出多线程的并行执行。如果我们将程序6-4中的第14行和第22行分别改为: 14 pthread_create(&tidi, NULL, sleeping, &i); 22 int sleep_time = * (int *)arg;即用传地址的方式给线程主函数传递参数。运行修改后的程序发现,五个子线程中的sleep_time的值均为0,而不是期望的0, 1, 2
35、, 3, 4。问题在于,多个线程存取一个共享变量(i)而缺少同步。因此,我们不能用这种方式来给线程传递参数。如果我们将程序6-4中的第14行和第22行分别改为: 14 pthread_create(&tidi, NULL, sleeping, i); 22 int sleep_time = (int )arg;则线程中sleep_time值分别为0, 1, 2, 3, 4,这正如所期望的。用传值的方法不会出现问题的原因是:系统将值的副本推入被调函数的栈中,因此该值就不会被其他线程修改了。传值的方式虽然是一种很好的方法,但是它不适合于传送结构或数组变量,对结构或数组变量必须用传地址的方式进行。解
36、决办法是:每次调用pthread_create()之前,用malloc() 临时分配要传递的参数变量的缓存,线程开始执行后取出缓存中的值,然后调用free()释放缓存。但是,malloc()和free()函数是不可重入的,即如果一个线程正在这两个函数的执行过程当中还没有退出,另一线程又调用了这两个函数,将会导致灾难性的后果,因为这两个函数操纵了相同的静态数据结构。但是,如果这两个函数是多线程安全(MT-safe)的,则可以这么使用。很多支持多线程的系统实现对传统的非线程安全的函数进行了改造,如对malloc()进行改造使得不同线程能够同时进入该函数,执行时提供了足够的同步,使得线程之间不相互干
37、扰,每个线程都带有一个独立的内存分配参数返回。对部分函数还提供了线程安全的版本,如ctime()函数的线程安全版本为ctime_r()。但是,一个函数是否是多线程安全完全取决于实现。如在Digital Unix中,printf函数不是多线程安全的,但在Solaris中却是多线程安全的。并且没有一个标准来规定网络API函数的线程安全性,完全取决于实现。一般来说,gethostbyname(), gethostbyaddr(), getservbyname(), getservbyport(), inet_ntoa()等都不是多线程安全的,使用时要特别注意。只有当一个函数的调用者给结果分配空间,并
38、将其指针作为参数传给另外一个函数时,这个函数才是线程安全的。对于非线程安全的函数,通常的解决办法是:使用以_r结尾的多线程安全的版本,如果不得不使用非线程安全的函数,则应尽量串行化调用来避免出现同时有多个线程访问的情况。此外,有些实现还提供了一种称为全局锁的机制来保护非线程安全的函数,如在Digital Unix中的pthread实现中,提供了两个全局锁函数pthread_lock_global_np()和pthread_unlock_global_np(),调用非线程安全函数之前先调用pthread_lock_global_np()加锁,函数执行完成后,调用pthread_unlock_gl
39、obal_np()解锁。尽管多线程安全函数可以在多线程程序中安全地使用,但是同其对应的非线程安全函数相比,它的效率要低一些,并且使用起来也不方便。此外,Posix还规定全局变量errno是唯一对每个线程分配的,使得线程同时进行系统调用时不互相干扰。但是,在多线程程序中一定要定义宏_REENTRANT或在编译时定义,否则在大多数实现中,所有线程仍然共用一个errno,而这将导致程序错误。程序6-4中创建线程时,将线程属性参数置为NULL,即使用系统默认值。但有的时候却是需要改变系统的默认值的,如提高线程的优先级,增加线程的栈空间,这时就需要设置线程属性。与线程属性相关的函数是pthread_at
40、tr_create和pthread_attr_delete。在pthread中,线程使用属性对象来表示线程属性,几个线程能与一个属性对象相连。如果一个对象的属性改变了,这种改变将在与此对象相关联的所有线程中表现出来(这一点与Solaris 2线程有所不同,Solaris 2线程是显式地设置线程属性和别的原始特性)。读者可参考有关线程设计的资料来了解设置属性的细节。2线程互斥和同步共享数据的同步是几乎所有多线程应用程序必须要有的部分。线程之间的同步通过互斥锁(mutex)和条件变量(condition) 来实现。互斥锁可以保证线程对共享变量的串行访问,不同的共享数据需要使用不同的锁来保护。条件变
41、量被用来阻塞一个线程直到该线程等待的条件成真(或指定的共享数据到达某一状态,状态是通过一个布尔表达式来定义,这个布尔表达式称为“谓词(predicate)”),并且条件变量总是同互斥锁一起使用。下面我们介绍一个使用线程同步函数(互斥和条件变量函数)的例子,这段代码是我们在Digital Unix平台上开发的一个大型软件系统中的一部分,所完成的功能是队列管理:初始化所有队列(InitializeMSWQueues();从队列中取出一个队列元素(OutQueueFrom(),往队列中插入一个元素(InToQueue()。系统中共使用了三个队列:空闲缓存队列(0),发送队列(1),接收队列(2)。空
42、闲队列存放可用的缓存块,发送队列存放要发送的信息,接收队列存放接收到的命令信息。系统中有一个线程专门从发送队列上取要发送的命令帧发送。所有线程从网络插口上接收到的信息都插入到接收队列,命令处理线程则从接收队列上取到达的命令帧进行处理,处理完成后释放缓存,即将缓存插入到空闲缓存队列中。如果有线程要发送信息,它首先从空闲缓存队列上申请一块可用缓存,填上要发送的信息,将其插入到发送队列中,由发送信息线程取走发送。发送信息线程每发送完一个命令帧,就将命令缓存插入到空闲缓存队列中。这样系统中的所有线程通过这三个队列协调地工作。队列所管理的信息由一个数组构成(而不是用传统的链表构成),队列元素为一块块大小
43、相等的缓存块(数据单元)。每个队列均有一个队列锁(qlock),一个条件变量(cond),一个条件变量锁(clock),与条件变量相关联的队列状态标志(num),队列缓存状态数组(pool),这些信息保存在队列控制信息结构中。队列控制信息结构类型定义如下:typedef struct pthread_mutex_t qlock; /* 队列锁 */ pthread_mutex_t clock; /* 条件变量锁 */ pthread_cond_t cond; /* 条件变量 */ int num; /* 与条件变量相联的队列状态标志 */ char poolMSW$MaxBuffer; /*
44、队列缓存状态数组 */MSW$QUEUE;变量num与条件变量联合使用,它指示队列的状态,num等于0表示队列中没有可用的缓存,为1表示有可用缓存单元。队列所管理的缓存数组是一个全局变量。每一个缓存数组单元的状态(占用或空闲)保存在队列结构中的状态数组pool中,即如果pooli等于0表示第i个缓存单元是空闲的,如果等于1表示第i个单元目前被占用。程序6-5 利用互斥锁和条件变量实现线程同步的队列管理程序。1 #include ./include/msw_common.h2 #include ./include/msw_buff_ext.h3 /*=4 * 初始化队列5 *=*/6 int I
45、nitializeMSWQueues()7 8 int which,loop;910 for(which=0;whichMSW$QUEUE_Num;which+)11 12 if (pthread_mutex_init(&QUEUE$headwhich.qlock,NULL)!=0)13 14 #ifdef DEBUG15 printf(初始化队列 %d 锁失败 !n,which);16 #endif17 return MSW$_Fail;18 19 if (pthread_mutex_init(&QUEUE$headwhich.clock,NULL)!=0)20 21 #ifdef DEBU
46、G22 printf(初始化队列 %d 条件变量锁失败 !n,which);23 #endif24 return MSW$_Fail;25 26 if (pthread_cond_init(&QUEUE$headwhich.cond,NULL)!=0)27 28 #ifdef DEBUG29 printf(初始化队列 %d 条件变量失败 !n,which);30 #endif31 return MSW$_Fail;32 3334 for (loop=0;loopMSW$MaxBuffer; loop+)35 36 if (which=0) /* 空闲缓冲区队列 */37 QUEUE$headwhich.poolloop=1;38 else /* 发送队列,接收队列 */39 QUEUE$headwhich