Skip to content

Latest commit

 

History

History
74 lines (53 loc) · 5.29 KB

ThreadPools.md

File metadata and controls

74 lines (53 loc) · 5.29 KB

大多数网络服务器,包括Web服务器都有一个特点,就是单位时间内要处理大量的请求,且处理的时间往往比较短。一般可以采用多进程或者多线程(包括线程池)来处理并发。

多进程

多进程网络服务器模型,其基本框架往往是,父进程用socket创建一个监听套接字,然后bindIP以及port,接着开始listen该套接字,通过一个while循环来accept连接,对于每一个连接,fork一个子进程来处理连接,并继续accept。简化后的代码如下:

//创建套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器接收的连接地址和监听的端口
//绑定(命名)套接字
bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
//创建套接字队列,监听套接字
listen(server_sockfd, 5);
//忽略子进程停止或退出信号
signal(SIGCHLD, SIG_IGN);

while(1)
{
    //接受连接,创建新的套接字
    client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr,
                           (socklen_t *)&client_len);

    if(fork() == 0)
    {
        //子进程中,读取客户端发过来的信息,处理信息,再发送给客户端
        close(client_sockfd);   // 处理完,关闭套接字
        exit(0);
    }
    else
    {
        //父进程中,关闭套接字
        close(client_sockfd);
    }
}

这种模型的缺点也十分明显,我们都知道fork一个子进程的代价是很高的,表现在以下几点:

  • 每次进来一个连接,操作系统为其创建一个进程,开销太大。因为子进程是父进程的副本,父进程和子进程共享正文段,子进程获得父进数据空间、堆和栈的副本。
  • 进程调度压力大。当并发量上来之后,系统会有N多个进程,这时候操作系统将花费相当多的时间来调度进程以及执行进程的上下文切换。
  • 每个进程都有自己独立的地址空间,需要消耗一定的内存,太多的进程会造成内存的大量消耗。同时,高并发下父子进程之间的IPC也是一个问题。

线程池

多线程网络服务器模型大致同上,不同点在于把每次accept一个新连接是创建一个线程而不是进程来处理。然而我们知道web服务器的一个特点就是短而高频率的请求,表现在服务器端就是不停地创建线程,销毁线程。那么有没有一种方法可以避免频繁地创建、销毁线程呢?当然有了,就是线程池

为了更好理解线程池,可以看下面的比喻:

  • 多线程:一个医院,每天面对成千上万的病人,处理方式是:来一个病人找来一个医生处理,处理完了医生也走了。当看病时间较短的时候,医生来去的时间,显得尤为费时了。
  • 线程池:设置门诊,把医生全派出去坐诊,病人来看病先挂号排队,医生根据病人队列顺序依次处理各个病人,这样就省去医生来来去去的时间了。但是,很多时候病人不多,医生却很多导致很多医生空闲浪费水电资源撒。
  • 可伸缩性线程池:也是用到线程池思想,这次门诊一开始只派出了部分医生,但是增加了一个领导,病人依旧是排队看病,领导负责协调整个医院的医生。当病人很多医生忙不过来的时候,领导就去多叫几个医生来帮忙;当病人不多医生太多的时候,领导就叫一些医生回家休息去免得浪费医院资源。

当然,如果线程创建和销毁时间相比任务执行时间可以忽略不计,则没有必要使用线程池了。以上面例子来说,如果一个医生每天只能看一个病人,那么就没有必要坐诊了。

要实现一个简单的线程池,只需要设计n个执行任务的线程,一个任务队列,然后按照下面的方式工作:

  1. 预先启动一些线程,线程负责执行任务队列中的任务,当队列空时,线程挂起。
  2. 调用的时候,直接往任务队列添加任务,并发信号通知线程队列非空。

要实现一个可伸缩的线程池,还需要添加一个管理线程,来负责监控任务队列和系统中的线程状态,当任务队列为空,线程数目多且很多处于空闲的时候,便通知一些线程退出以节约系统资源;当任务队列排队任务多且线程都在忙,便负责再多启动一些线程来执行任务,以确保任务执行效率。

Linux_OS_ThreadPool 是一个简单的 C 实现的线程池的例子,Test.cpp 是一个简单的测试例子, 其中:

  • pool_add_worker():线程池的任务链表中加入一个任务,加入后通过调用 pthread_cond_signal(&(pool->queue_ready)) 唤醒一个出于阻塞状态的线程(如果有的话)。
  • pool_destroy():销毁线程池,线程池任务链表中的任务不会再被执行,但是正在运行的线程会一直把任务运行完后再退出。

一个更加完善的 C 实现可以在 C-Thread-Pool 找到。

更多阅读

简单Linux C线程池的实现
使用 libevent 和 libev 提高网络应用性能