LinuxC线程池的实现在学习网络编程的时候,自己实现一个WebServer是一种很有趣的体验。大多数Web服务器都有一个特点:它们需要在单位时间内处理大量的请求,而且处理这些请求的时间往往很短。《深入理解计算机系统》(CSAPP)在讲解网络编程时实现了一个经典的WebServer。这个WebServer不仅可以满足静态请求,还可以满足动态请求(CGI)。虽然这个WebServer可以正常使用,但是还是有一个明显的缺陷:它是一个迭代的WebServer,也就是说在一个请求处理完之前,不能同时处理另一个请求,而我们前面提到的WebServer一个重要的特点是单位时间内可能会有大量的请求,所以如果放到工业世界中,这种情况自然是不能容忍的。多进程WebServer模型解决了上述WebServer只能一个一个处理请求的问题。第一种方案是:在接受请求时,fork一个子进程来处理请求,同时主进程还在监听是否有新的连接请求。从表面上看,多进程模型似乎解决了问题,但我们都知道fork一个进程的开销是非常大的,基于以下事实。从概念上讲,fork可以认为是创建父进程程序段、数据段、堆段和栈段的副本。但是如果真的只是简单的把父进程的虚拟内存页复制给子进程,那就太浪费了。现代UNIX(Linux)在实现fork时经常使用两种技术来避免这种浪费。一种是内核将每个进程的代码段标记为只读,这样无论是父进程还是子进程都不能修改代码段。这样父进程和子进程就可以共享同一个代码段。二是对于父进程的数据段、堆段、栈段中的每一页,内核都采用了写时复制(copy-on-write)的方式。这样做的原因之一是:fork后面经常跟着exec,它会用新程序替换一个进程的代码段,并重新初始化它的数据、堆和堆栈段。但不管怎么说,还是有复制页表的操作,这就是为什么在UNIX(Linux)下创建进程比创建线程更昂贵的原因。当并发量大的时候,此时系统中会有大量的进程,会导致CPU花费大量的时间进行进程调度,进程上下文的切换开销也很大。因此,与多进程模型相比,多线程是一个更好的模型:创建线程比创建进程更快,线程之间的上下文切换通常比进程花费更少的时间。多线程WebServer模型被多线程WebServer模型所取代:每接受一个请求,就创建一个线程,由线程处理请求。切换到多线程模型可以解决fork带来的开销问题,但是调度问题依然存在。因此,一个显而易见的解决方案是使用线程池来固定线程数。基本实现思路如下。每个请求都被封装成一个Job,每个Job都包含了线程要执行的方法,传递给线程的参数,以及用来描述Job在Job队列中位置的参数。线程池维护一个作业队列,每个线程从作业队列中取出一个作业执行。因为Job队列是共享资源,所以需要控制线程同步。线程池初始化时,立即创建一定数量的线程。此时,这些线程因为Job队列为空而被阻塞。代码实现tinyhttpd是我为了更有效的学习网络编程而实现的一个轻量级的WebServer,还有一些问题需要解决和优化。根据以上思路,我实现了一个简单的线程池,并引入到tinyhttpd中。具体代码实现请参考threadpool.h和threadpool.c。遗留问题当线程池中的线程数固定时,仍然存在一个严重的问题:现实中很多连接都是长连接,这意味着当一个线程处理请求时,读取的数据会不正确。不断地。当线程处理完一批数据后,如果继续读取,但是下一批数据还没有到来,由于文件描述符默认是阻塞的,线程就会进入阻塞状态。因此,如果线程池中的所有线程都被阻塞,此时如果有新的请求到达,则无法处理。解决方法是将文件描述符设置为非阻塞,并使用事件驱动(Event-driven)来处理连接。参考Linux/UNIX系统编程手册深入理解计算机系统zaver
