并发编程的目的是让程序“同时”执行多个任务,如果程序是计算密集型的,并发编程就没有优势,反而由于任务的频繁切换导致效率降低。如果程序是I/O密集型的,如经常读写文件,访问数据库,那就另当别论:由于I/O操作没有CPU计算的速度快,所以让程序阻塞与I/O操作将浪费cpu时间,如果程序有多个线程执行,则当前被I/O阻塞的执行线程可主动放弃CPU(或由操作系统来调度)并将程序的执行权转移到其他线程,服务器两种并发编程模式:半同步/半异步,领导者/追随者(这里的同步和异步与I/O模型中的同步和异步是完全不同的概念。在I/O模型中:“同步”和“异步”的区分是内核向应用程序通知何种I/O事件(就绪事件还是完成事件),及有谁来完成I/O的读写(是应用程序还是内核);并发模式中:同步指的是完全按照代码顺序执行,异步指的是程序的执行由系统事件来驱动,常见的系统事件:信号,中断等)。
并发模式是指:I/O处理单元和多个逻辑单元之间协调完成任务的办法.
半同步/半异步
程序并发会带来上下文切换问题——进程或线程切换导致的系统开销,即使是I/O密集的服务器也不应使用多工作线程,线程的切换会消耗大量的cpu时间,所以用此方式较好,允许一个线程同时处理多个连接,多线程服务器的另一个有点是不同线程运行在不同cpu上,当前线程数目不大于CPU数目时,上下文的切换就不成问题。
异步代码虽然执行效率高,实时性强,但是复杂,难以扩展,不适合大量并发。所以在此模式中,同步线程用于处理客户逻辑(相当于服务器框架中的逻辑单元),异步用于处理I/O事件,相当于服务器框架中的I/O单元。异步线程监听到客户请求后,将其封装成请求对象插入到请求队列中,请求队列将通知某个工作在同步模式中的工作线程来处理并处理该请求对象。
变体一:半同步/半反应堆
主线程为异步线程,监听所有socket上的事件。如果监听socket上有读事件发生:代表有新的连接请求,主线程接受得到新的连接socket,然后往epoll内核事件表中注册socket上的读写事件;如果连接socket上有读写事件发生,即有新的客户请求到来或有数据发送至客户端,主线程就将该连接socket插入请求队列。所有的工作线程都睡眠在请求队列上,通过竞争获得任务的接管权,这种机制使得空闲线程才有权利处理新到来的任务。
该模式采用的是Reactor模式,主线程插入请求队列中的任务是就绪的连接socket,他要求工作线程从自己socket上读取客户请求和王socket写服务器应答。也可以由Proactor来完成:主线程完成数据的读写,然后将应用程序数据,任务类型信息封装为一个任务对象,然后将其(或指向该任务对象的一个指针)插入请求队列,工作线程从请求队列中获取任务对象后直接处理。
缺点:
- 主线程和工作线程共享请求队列,主线程往请求队列中添加任务或工作线程从请求队列中取任务,队列都需要加锁保护,浪费CPU
- 每个工作线程在同一时刻只能处理一个客户请求,如果客户请求较多然而工作线程较少,则请求队列中将累积多个任务对象,客户端的响应速度变慢,如果通过增加工作线程来解决这一问题,工作线程的来回切换也浪费CPU时间
变体二:高效的半同步/半异步模式
为解决上述问题,使每个工作线程都能处理多个客户连接,主线程只管理监听socket,链接socket由工作线程管理,当有新的连接到来时,主线程就接受之并返回新的连接socket配发给工作线程(往他和工作线程之间的管道写数据,工作线程分析是否有新的客户连接请求到来,如果是,就把新socket上的读写事件注册到自己的epoll内核事件表中),此后该新socket上的任何I/O操作都由被选中的工作线程来处理,直到客户端关闭连接。每个工作线程和主线程都维持有自己的事件循环,各自独立的监听不同的事件。
领导者/追随者
多个工作线程轮流获得事件源集合,轮流监听,分发并处理事件的一种模式,任意时间点,程序都仅有一个领导者线程,他负责监听I/O事件,其他线程都是追随者,他们休眠在线程池中等待成为新的领导者,当前领导者如果检测到I/O事件,首先从线程池中选出新的领导线程,然后处理I/O事件,此时原来的领导者等待新的I/O事件,新的领导者处理I/O事件,二者实现了并发。
- 句柄:用于表示I/O资源,linux下为一个文件描述符
- 句柄集:管理众多句柄,用wait_for_event来监听句柄上的I/O事件,并将就绪的事件通知给领导者线程,领导者线程通过绑定到handle上的事件处理器来处理事件(handle和事件处理器的绑定是通过句柄集中的regeiregister_handle实现)
- 线程集:所有工作线程(领导者,追随者)的管理者,负责各个线程间的同步及选出新的领导者,线程集中的线程任意一时刻处于一下三种状态:1>处于领导者身份,负责等待句柄集上的I/O事件。2>Processing:线程正在处理事件,领到线程检测到I/O事件后转移到该状态来处理事件,并调用promote_new_leader推选新的领导者,也可以指定其他追随者处理事件(Event handoff),此时领导地位不变,当处于processing状态的线程处理完该事件后,如果当前线程没有领导者那么他就变为新的领导者,否则变为追随者。3>当前线程处于追随者身份,通过调用线程集的join函数成为新的领导者,也可呗当前领导者指定处理任务。
- 事件处理器:事件处理器通常包含一个或多个回调函数handle_event,这些回调函数用于处理事件对应的逻辑业务,事件处理器使用前需要绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器上的回调函数。
- 具体的事件处理器:是事件处理器的派生类,他必须重新实现基类的handle_event以处理特定的任务。
由于领导者线程需要自己监听I/O事件并处理客户请求,所以此模式下之间不需要传递额外的数据,也不用向半同步/半反应堆模式那样在线程之间同步对请求队列的访问,但是仅支持事件源集合,无法让每个线程独立的管理多个客户的连接。
本文和Reactor,Proactor主要针对I/O,请求队列,和逻辑单元之间协调完成各种任务的模式。