Linux C 线程池实现
  学习网络编程时,自己动手实现一个 Web Server 是一个很有意思的经历。大多数 Web Server 都有一个特点:在单位时间内需要处理大量的请求,并且处理这些请求的时间往往还很短。《深入理解计算机系统》 ( CSAPP ) 在讲解网络编程时实现了一个经典的 Web Server ,这个 Web Server 不仅满足了静态请求,同时还满足了动态请求 ( CGI )。虽然这个 Web Server 能够正常使用,但是仍存在一个明显的缺陷:它是一个迭代式的 Web Server ,这意味着在一个请求处理完毕前,不能同时处理另一个请求,而我们之前提到 Web Server 的一个重要特点是在单位时间内可能会有大量的请求,所以如果投入工业界,这种情况自然是无法容忍的。
  多进程 Web Server 模型
  解决上面提到的 Web Server 只能一个接着一个处理请求的第一个方案是:当 accept 到一个请求时, fork 一个子进程去处理这个请求,而主进程仍然在监听是否有新的连接请求。多进程模型在表面上看似乎解决了问题,但是我们都知道 fork 一个进程的开销是非常大的,基于以下几个事实。
  从概念上说,可以将 fork 认作对父进程程序段、数据段、堆段以及栈段创建拷贝。但是如果真的只是简单的将父进程虚拟内存页拷贝到子进程,那太浪费了。现代 UNIX ( Linux ) 在实现 fork 时往往会采用两种技术来避免这种浪费。一是内核将每一进程的代码段标记为只读,从而使得父进程和子进程都无法修改代码段。这样,父进程和子进程可以共共享同一代码段。二是对于父进程数据段、堆段和栈段中的各页,内核采用写时复制( copy-on-write ) 的方式,这么做的原因之一是: fork 之后常常伴随着 exec ,这会用新程序替换进程的代码段,并重新初始化其数据段、堆段和栈段。但是无论如何,仍存在复制页表的操作,这也是为什么在 UNIX ( Linux ) 下创建进程要比创建线程开销大的原因。
  并发量一大,此时系统内便会有存在大量的进程,这会导致 CPU 花费大量的时间在进程调度上,并且进程上下文的切换开销也很大。
  因此,相比于多进程模型,多线程是一个更优的模型:创建线程要快于创建进程,线程间的上下文切换消耗的时间一般也比进程要短。
  多线程 Web Server 模型
  换用多线程 Web Server 模型:每 accept 一个请求,创建一个线程,将请求交由该线程处理。换用多线程模型可以解决由 fork 带来的开销问题,但是调度问题依然还是存在的。因此,一个显而易见的解决办法是使用线程池,将线程的数量固定下来。基本的实现思路如下。
  将每个请求封装为一个 Job ,每个 Job 包含线程要执行的方法、传递给线程的参数以及用于描述该 Job 处于 Job 队列的位置的参数。
  线程池维护着一个 Job 队列,每个线程从 Job 队列中取下一个 Job 执行。因为该 Job 队列是一个共享资源,因此需要控制线程的同步。
  初始化线程池时,马上创建一定数量的线程。此时,这些线程都是阻塞状态的,因为 Job 队列为空。
  代码实现
  tinyhttpd 是我为了更有效的学习网络编程而实现的一个轻量级的 Web Server ,目前仍有部分问题需要解决以及优化。按照上面的思路,我实现了一个简单的线程池,并将其引入到 tinyhttpd 中。具体的代码实现请参考 threadpool.h 和 threadpool.c 。
  剩余问题
  当固定了线程池的线程数量后,仍然存在一个严重的问题:实际情况下,很多连接都是长连接,这意味着一个线程在处理一个请求时, read 到的数据将会是是不连续的。当线程处理完一批数据后,如果继续 read ,而下一批数据还未到来时,由于默认情况下 file descriptor 是 blocking 的,因此该线程会进入阻塞状态。所以,如果线程池中所有的线程都处于阻塞状态,此时如果有新的请求到来,那么是无法处理的。
  解决方案是将 file descriptor 设置为 non-blocking ,利用事件驱动( Event-driven )来处理连接。