网络服务在处理数以万计的客户端链接时,每每出现效率低下甚至彻底瘫痪,这被 称为 C10K 问题。C10K问题最先提出于2003年,10多年间,随着互联网的迅速发展,愈来愈多的网络服务面临的再也不是C10K问题,而是C10M问题!html
1. 每一个请求建立一个线程,使用阻塞式 I/O 操做react
这是最简单的线程模型,1个线程处理1个链接的所有生命周期。该模型的优势在于:这个模型足够简单,它能够实现复杂的业务场景,同时,线程个数是能够远大于CPU个数的。然而,线程个数又不是能够无限增大的,为何呢?由于线程何时执行是由操做系统内核调度算法决定的,调度算法并不会考虑某个线程可能只是为了一个链接服务的,时间片到了就执行一下,哪怕这个线程一执行就会不得不继续睡眠。这样来回的唤醒、睡眠线程在次数很少的状况下,是廉价的,但若是操做系统的线程总数不少时,它就是昂贵的(被放大了),由于这种技术性的调度损耗会影响到线程上执行的业务代码的时间。举个例子,当咱们所追求的是并发处理数十万链接,当几千个线程出现时,系统的执行效率就已经没法知足高并发了。换言之,该模型的扩展性及其糟糕,根本没法有效知足高并发,海量链接的业务场景。linux
2. 使用线程池,一样使用阻塞式 I/O 操做算法
这是针对模型1的改进,但仍未从根本上解决问题编程
3. 使用非阻塞I/O + I/O复用windows
4. Leader/Follower 等高级模式 服务器
对高并发编程,目前只有一种模型,也是本质上惟一有效的玩法。网络链接上的消息处理,能够分为两个阶段:等待消息准备好、消息处理。当使用默认的阻塞套接字时(例如上面提到的1个线程捆绑处理1个链接),每每是把这两个阶段合而为一,这样操做套接字的代码所在的线程就得睡眠来等待消息准备好,这致使了高并发下线程会频繁的睡眠、唤醒,从而影响了CPU的使用效率。网络
高并发编程方法固然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。固然,这也要求套接字必须是非阻塞的,不然,处理消息的代码段很容易致使条件不知足时,所在线程又进入了睡眠等待阶段。那么问题来了,等待消息准备好这个阶段怎么实现?它毕竟仍是等待,这意味着线程仍是要睡眠的!解决办法就是,线程主动查询,或者让1个线程为全部链接而等待!这就是IO多路复用了。多路复用就是处理等待消息准备好这件事的,但它能够同时处理多个链接!它也可能“等待”,因此它也会致使线程睡眠,然而这没关系,由于它一对多、它能够监控全部链接。这样,当咱们的线程被唤醒执行时,就必定是有一些链接准备好被咱们的代码执行了。多线程
做为一个高性能服务器程序一般须要考虑处理三类事件: I/O事件,定时事件及信号。本文将首先首先从总体上介绍两种高校的事件处理模型:Reactor和Proactor。并发
首先来回想一下普通函数调用的机制:程序调用某函数,函数执行,程序等待,函数将结果和控制权返回给程序,程序继续处理。Reactor释义“反应堆”,是一种事件驱动机制。和普通函数调用的不一样之处在于:应用程序不是主动的调用某个API完成处理,而是偏偏相反,Reactor逆置了事件处理流程,应用程序须要提供相应的接口并注册到Reactor上,若是相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。
Reactor模式是处理并发I/O比较常见的一种模式,中心思想就是,将全部要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(区别在于多路复用器是边沿触发仍是水平触发),多路复用器返回并将相应I/O事件分发到对应的处理器中。
Reactor模型有三个重要的组件:
具体流程以下:
1. 注册读就绪事件和相应的事件处理器;
2. 事件分离器等待事件;
3. 事件到来,激活分离器,分离器调用事件对应的处理器;
4. 事件处理器完成实际的读操做,处理读到的数据,注册新的事件,而后返还控制权。
Reactor模式是编写高性能网络服务器的必备技术之一,它具备以下的优势:
Reactor模型开发效率上比起直接使用IO复用要高,它一般是单线程的,设计目标是但愿单线程使用一颗CPU的所有资源,但也有附带优势,即每一个事件处理中不少时候能够不考虑共享资源的互斥访问。但是缺点也是明显的,如今的硬件发展,已经再也不遵循摩尔定律,CPU的频率受制于材料的限制再也不有大的提高,而改成是从核数的增长上提高能力,当程序须要使用多核资源时,Reactor模型就会悲剧, 为何呢?
若是程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就能够直接开启多个反应堆,每一个反应堆对应一颗CPU核心,这些反应堆上跑的请求互不相关,这是彻底能够利用多核的。例如Nginx这样的http静态服务器。
若是程序比较复杂,例如一块内存数据的处理但愿由多核共同完成,这样反应堆模型就很难作到了,须要昂贵的代价,引入许多复杂的机制。
具体流程以下:
从上面的处理流程,咱们能够发现proactor模型最大的特色就是Proactor最大的特色是使用异步I/O。全部的I/O操做都交由系统提供的异步I/O接口去执行。工做线程仅仅负责业务逻辑。在Proactor中,用户函数启动一个异步的文件操做。同时将这个操做注册到多路复用器上。多路复用器并不关心文件是否可读或可写而是关心这个异步读操做是否完成。异步操做是操做系统完成,用户程序不须要关心。多路复用器等待直到有完成通知到来。当操做系统完成了读文件操做——将读到的数据复制到了用户先前提供的缓冲区以后,通知多路复用器相关操做已完成。多路复用器再调用相应的处理程序,处理数据。
Proactor增长了编程的复杂度,但给工做线程带来了更高的效率。Proactor能够在系统态将读写优化,利用I/O并行能力,提供一个高性能单线程模型。在windows上,因为没有epoll这样的机制,所以提供了IOCP来支持高并发, 因为操做系统作了较好的优化,windows较常采用Proactor的模型利用完成端口来实现服务器。在linux上,在2.6内核出现了aio接口,但aio实际效果并不理想,它的出现,主要是解决poll性能不佳的问题,但实际上通过测试,epoll的性能高于poll+aio,而且aio不能处理accept,所以linux主要仍是以Reactor模型为主。
在不使用操做系统提供的异步I/O接口的状况下,还可使用Reactor来模拟Proactor,差异是:使用异步接口能够利用系统提供的读写并行能力,而在模拟的状况下,这须要在用户态实现。具体的作法只须要这样:
咱们知道,Boost.asio库采用的即为Proactor模型。不过Boost.asio库在Linux平台采用epoll实现的Reactor来模拟Proactor,而且另外开了一个线程来完成读写调度。
在《Linux高性能服务器编程》一书中(PS:一本好书,推荐购买阅读!)为咱们提供一种精妙的设计思路:
两个模式的相同点,都是对某个IO事件的事件通知(即告诉某个模块,这个IO操做能够进行或已经完成)。在结构上二者也有相同点:demultiplexor负责提交IO操做(异步)、查询设备是否可操做(同步),而后当条件知足时,就回调注册处理函数。
不一样点在于,异步状况下(Proactor),当回调注册的处理函数时,表示IO操做已经完成;同步状况下(Reactor),回调注册的处理函数时,表示IO设备能够进行某个操做(can read or can write),注册的处理函数这个时候开始提交操做。
至于两种模式孰优孰劣的问题,笔者觉得差别并非特别大。两种模式的设计思想均足以很好的胜任高并发,海量链接的应用要求。固然,就目前笔者有限的了解,Reactor的应用实例仍是更多一些,尤为是在Linux平台下。
笔者水平有限,疏谬之处,万望斧正!
本文有至关分量的内容参考借鉴了网络上各位网友的热心分享,特别是一些带有彻底参考的文章,其后附带的连接内容更直接、更丰富,笔者只是作了一下概括&转述,在此一并表示感谢。
《Linux多线程服务器编程》
《Linux高性能服务器编程》