上文中使用了预先派生一个子进程池来处理客户端的连接,这会比为每个客户端连接现场派生一个子进程快的得多。但是进程的创建和调度比起线程来说,还是比较慢的。
在多线程并发服务器中可以为每个客户端的连接创建一个线程,我们也可以使用线程池来取代为每个客户端现场创建一个连接。并让每个线程各自调用accept
。取代每个线程都阻塞在accept
调用中的做法,使用互斥锁以保证任何时刻只有一个线程在调用accept
。
TCP预先创建线程服务器程序,每个线程各自accept
代码如下:
1 |
|
main函数
调用tcp_listen
函数创建一个监听套接字。调用thread_make
预先创建线程,创建的个数由命令行指定,最大的个数不能超过MAX_CHILDREN
。SIGINT
信号处理函数,在终止服务器(CTRL+C)时被调用,用于统计各个线程处理的连接数。
thread_make函数
创建线程,执行thread_main
函数。
thread_main函数
每个线程在调用accept
时,需要获得互斥锁,如果没有获得互斥锁,本线程将阻塞,然后调用accept
返回一个已连接套接字,调用process_request
函数处理客户请求,最后关闭连接。
互斥锁
多个线程更改一个共享的变量,其解决办法是使用一个互斥锁保护这个共享变量,访问该变量的前提条件是持有该互斥锁。互斥锁是类型为pthread_mutex_t
的变量。使用以下两个函数为一个互斥锁上锁和解锁。
1 |
|
如果试图上锁已被另外某个线程锁住的一个互斥锁,本线程将被阻塞,直到该互斥锁被解锁为止。
如果某个互斥锁变量是静态分配的,就必须把它初始化为常值PTHREAD_MUTEX_INITIALIZER
,如果在共享内存区中分配一个互斥锁,那么必须通过调用pthread_mutex_init
函数在运行时把它初始化。
sig_int函数
在服务器终止(CTRL+C)时被调用,用于打印在每个线程处理的连接数。
运行服务器程序:
服务器程序创建了10个线程,输出如下信息:
[heql@ubuntu socket]$ ./server 10
thread 5 starting
thread 6 starting
thread 4 starting
thread 7 starting
thread 3 starting
thread 8 starting
thread 9 starting
thread 2 starting
thread 1 starting
thread 0 starting
运行客户端程序:
使用上文中的客户端程序:客户端使用5个子进程各自发起5000次连接。在每个连接上,客户向服务器发送请求4096字节数据,服务器将向每个连接的客户返回4096字节数据。
[heql@ubuntu socket]$ ./test_client 192.168.1.156 9987 5 5000 4096
child 1 done
child 0 done
child 3 done
child 2 done
child 4 done
终止服务器
客户端程序运行完后,按下CTRL+C终止服务器,查看每个线程处理的连接个数:
^C
child 0, 2461 connections
child 1, 2483 connections
child 2, 2531 connections
child 3, 2511 connections
child 4, 2519 connections
child 5, 2510 connections
child 6, 2486 connections
child 7, 2505 connections
child 8, 2470 connections
child 9, 2524 connections
TCP预先创建线程服务器程序,主线程统一accept
在程序启动阶段创建一个线程池之后,也可以只让主线程调用accept
并把每个客户连接传递给线程池中的某个可用线程。
代码如下:
1 |
|
定义存放已连接套接字描述符的共享数组
定义一个client_fd
数组,由主线程往其中存入已接受连接的套接字描述符,并由线程池中的可用线程从中取出一个以服务相应的客户端。back
是主线程将往该数组中存入的下一个元素的下标,front
是线程池中某个线程将从该数组中取出的下一个元素的下标。使用互斥锁和条件变量对这些共享的数据结构进行保护。
条件变量
当需要一个让主循环进入睡眠,直到某个线程通知它有事可做才醒来的方法。条件变量结合互斥锁能够提供这个功能。互斥锁提供互斥机制,条件变量提供信号机制。
条件变量是类型为pthread_cond_t
的变量。以下两个函数使用条件变量。
1 |
|
pthread_cond_wait
函数把调用函数投入睡眠并释放调用线程持有的互斥锁,当调用线程后来从pthread_cond_wait
返回时,该线程再次持有互斥锁。
为什么每个条件变量都要关联一个互斥锁呢?因为”条件”通常是线程之间共享的某个变量的值。允许不同线程设置和测试该变量,要求有一个与该变量关联的互斥锁。举例来说,下面的代码没有互斥锁,那么主循环将如下测试变量ndone。
1 | while(ndone == 0) { |
这里存在如此可能性:主线程外最后一个线程在主循环测试ndone==0之后,但在调用pthread_cond_wait
之前递增ndone。如果发生这样的情形,最后那个”信号”就丢失了,造成主循环永远阻塞在pthread_cond_wait
调用中,等待永远不再发生的某事再次出现。
pthread_cond_signal
通常唤醒等在相应条件变量上的单个线程。有时候一个线程知道自己应该唤醒多个线程,这个情况下它可以调用pthread_cond_broadcast
唤醒等在相应条件变量上的所有线程。
1 |
|
main函数
主线程大部分时间阻塞在accept
调用中,等待各个客户连接的到达。一旦客户连接到达,主线程就把它的已连接套接字描述符存入client_fd
数组中,不过需要获取保护该数组的互斥锁。主线程还检查back
下标没有赶上front
下标(若赶上则说明该数组不够大)。并发送信号到条件变量信号,然后释放互斥锁,已允许线程池中某个线程为这个客户端服务。
thread_main函数
线程池中的每个线程都试图获取保护client_fd
数组的互斥锁。获得之后就测试back
与front
,若两者相等,通过调用pthread_cond_wait
睡眠在条件变量上。主线程接受一个连接后将调用pthread_cond_signal
向条件变量发送信号,以唤醒睡眠在其上的线程。若back
和front
不等,则从client_fd
数组中取出下一个元素以获得一个连接,然后调用process_request
。
实际上这个版本会稍微慢于上面中先获取一个互斥锁再调用accept
的版本。原因在于这个版本需要同时获取互斥锁和条件变量。