C++服务器设计(三):多线程模型设计

多线程探讨

  现在大多数CPU都具备多个核心,为了最大程度的发挥多核处理器的效能,提升服务器的并发性,保证系统对于多线程的支持是十分必要的。咱们在以前的设计都是基于单线程而言,在此章咱们将对系统进行改进,在进一步提高系统性能的同时保证系统对于多线程的支持。安全

  首先考虑这么几个问题,咱们以前已经选定了基于I/O复用的Reactor模式,那么在多线程环境下咱们该如何处理这些I/O?多线程同时处理同一个套接字描述符安全吗?Reactor模式支持多线程吗?服务器

  根据查阅文档可知,针对文件描述符的常见系统调用如read、write是线程安全的,咱们不用担忧多个线程同时操做文件描述符会致使进程崩溃发生。同时根据UNPv1描述,在两个线程中分别对同一个套接字进行read操做和write操做是线程安全的,由于TCP套接字是双向I/O。数据结构

  可是咱们依然要考虑以下的集中状况:多线程

  • l  两个线程同时读同一个套接字,此时两个线程各自收到同一条消息的一部分数据,如何把这两部分数据合并成一条完整的消息?
  • l  两个线程同时写同一个套接字,此时每一个线程都只发送出去了半条消息,接收方将如何处理接收到的数据?

  若是咱们给每一个套接字配一把锁,让每次只能有一个线程得到锁来读或者写这个套接字,这可以解决以上的问题。可是在Reactor模式中,咱们应该尽可能避免阻塞线程的操做。若是此时某个线程中的事件处理器竞争锁失败被阻塞,将会致使该线程以后的其余事件处理也所有被阻塞。并发

  所以,咱们认为虽然描述符常见系统调用是线程安全的,可是因为将一个描述符置于多线程环境中将会使整个业务逻辑复杂化,虽然必定程度上咱们能够经过应用层I/O缓冲加锁机制解决,可是这依旧会致使线程阻塞现象和服务器性能降低,这是得不偿失的。所以咱们认为在多线程环境下咱们依然要确保每一个文件描述符只能有一个线程进行操做。这样既可解决消息收发的顺序问题,同时也避免了各类锁竞争现象。函数

  在以上符合以上原则的状况下,咱们将每一个链接套接字的读写操做依旧注册在单一Reactor反应器中。同时咱们在以前章节描述过,每一个Reactor模式都包含一个线程大循环,所以每一个Reactor反应器都应该是单线程的,能够支持注册多个链接套接字。可是若是将全部链接套接字都注册在一个线程中,咱们的系统就退化为了单线程服务器了。所以咱们应该将每个新链接平均分配到不一样的反应器事件循环中,让多个线程平均注册不一样的链接事件,让每一个线程处理该线程内的全部反应器事件。oop

One loop per thread模型介绍

  根据以前分析,咱们服务器系统的多线程模型已经大体清晰,即采用non-blocking IO + one loop per thread模式。在该模式下,建立多个线程,而且每一个线程都建立Reactor反应器,每一个反应器又存在一个事件循环(event loop),用于等待注册事件和处理事件的读写。当咱们须要让哪一个线程干活,咱们就把某个新的链接套接字注册到该线程所在的反应器中便可。post

  在这种模式中,虽然咱们要注意每一个套接字只能注册到一个线程反应器中,不能跨反应器使用,可是这种能够分配套接字所在线程的方式依旧可以给咱们的系统带来很大的负载弹性。好比对于实时性要求较高的链接能够单独占用一个线程;处理数据量大的链接也能够独占一个线程,并把某些数据处理任务分摊到另外几个计算线程中;而某些相对次要的辅助性链接能够多个共享一个线程,只要保证每一个链接的处理器无阻塞,依旧可以保证事件处理延迟并不会过高。性能

  咱们能够将这种模式的优势总结以下:线程

  • l  线程数量在程序启动时设置,数量肯定,并经过线程池管理,不会频繁建立与销毁线程的开销。
  • l  能够很方便的在线程间调节负载。
  • l  对于同一个TCP链接而言,整个链接期间所在线程固定,没必要考虑事件并发的可能。

线程间任务队列模型设计

  线程间任务队列模型是一种多线程处理的形式。处理过程当中将须要在某个线程中运行的任务注册到该线程的任务队列中,当该线程检测到任务队列中存在任务时,将会取出任务并执行。当任务队列中的任务所有都执行完毕后,该线程将会被阻塞,直到有新的任务被注册致使线程被唤醒。

  首先咱们不考虑Reactor模式,设计一个符合以上需求的模型。在这个模型中线程的关键数据结构是任务队列。

  任务队列相似于缓冲无限大的多生产者多消费者模型。缓冲经过条件变量进行多线程保护。生产者和消费者均在不一样线程中,生产者经过post操做向缓冲尾部添加任务,消费者经过take操做从缓冲头部获取任务。可能存在多个生产者的状况,所以若是有某个生产者指望添加任务,须要获取同步锁后才能进行添加操做。而消费者不但须要获取同步锁,并且还要检查当前任务队列是否存在可用任务,若是存在则取出,若是不存在则经过条件变量被阻塞,直到存在某个生产者添加了新的任务并执行唤醒操做。

  任务队列的缓冲部分应该支持从头部读取,从尾部写入的队列功能。同时最好可以支持缓冲动态增加,使其在生产者的角度看来缓冲应该相似无限大,以保证不会出现生产者写入过多任务致使操做被阻塞的状况。在STL库中,deque结构做为动态增加分段连续的双向容器,能够很好的知足以上需求,所以咱们采用STL库的std::deque做为缓冲实现。

  同时缓冲的任务数据部分,相似于以前章节咱们分析的回调函数。它是对象,可以以数据结构的形式被写入缓冲中,当从缓冲中读取出来后,它又可以以相似于函数的形式被调用,最好还能带有自身参数的管理。boost库中的function<void>函数对象实现了这一功能,它能够经过普通函数赋值,也能够经过同为boost库中的bind函数绑定带参数函数或某个成员函数。它可以被当作一个对象供缓冲容器保存,同时也可以做为回调函数被执行。

  经过以上研究设计,咱们的任务队列是一个以条件变量进行多线程保护的缓冲,该缓冲的底层数据结构实现为std::deque<boost::function<void()> >。

图3-12 线程间任务队列模型

  最终的线程间任务队列设计实现如图所示。线程主体是一个任务循环,它会反复从任务队列中take可用任务。若是当前任务队列没有任务,take操做将使线程阻塞,直到其余线程中添加了新的可用任务到该任务队列,才会将该线程唤醒并获取任务。当线程获取到任务后,将会在本线程中执行任务回调。当任务执行结束后,线程将从新进入循环,再次期待从任务队列中take到可用任务。

  经过该线程间任务队列模型,咱们能够将指望的任务操做从某个线程转移到另外一个线程中执行。

线程模型与Reactor模式结合

  以前的线程间任务队列模型设计中,咱们并无考虑到Reactor模式的特性,更没有联系到服务器系统的具体需求场景中。所以咱们仍须要对该模型进行改造,使之融入到咱们的整个服务器系统中。

  Reactor模式下的系统原型相似于图3-6,其主体是事件循环下的事件分离器监听事件产生,并回调具体事件的handler进行处理。咱们给每一个反应器添加一个任务队列结构,用于缓冲其余线程向该线程注册的任务。同时咱们须要知道其余线程是什么时候向该Reactor反应器添加了任务。由于以前的Reactor反应器并不能监放任务队列的数量,而且Reactor可能会被阻塞在epoll事件监听中,若是长期没有事件被监听,整个反应器线程将会被长期阻塞,即便此时有其余线程向该反应器添加了任务,也没法获得及时执行。

  咱们经过给每一个反应器额外建立一个管道,并将该管道的描述符可读事件注册到该反应器中。该描述符一样向其余线程暴露,当其余线程经过该反应器的任务队列向其添加了新任务后,再获取该反应器的管道描述符,并执行写操做。此时咱们只需写入随便一字节数据,目的是唤醒可能处于事件监听而阻塞的该反应器,通知它任务队列存在可用任务,须要执行处理。

 

图3-13 支持任务队列的Reactor反应器模型

  咱们设计的支持任务队列的Reactor反应器如图3-13所示。在反应器初始化阶段建立一个管道,并将该管道描述符注册到反应器中,以便其余线程可以唤醒该反应器。同时在反应器处理完全部激活事件的handler后,会检查自身的任务队列是否为空。在这里不一样于以前的线程模型设计,若是任务队列为空,代表当前没有任务可执行,反应器不可以被阻塞于此,而是直接跳过进入下一轮循环;若是任务队列非空,就把任务队列中的全部任务所有读取出来,并依次回调执行,执行完后进入下一轮循环。由于反应器的要求就是尽量的非阻塞,它的核心是事件处理,而咱们的任务队列相似于承属于管道描述符的特殊事件处理。所以对于该事件而言,它不一样于线程模型,是有任务则处理,无任务则跳过。

  而在其余线程中,若是想对某个反应器添加任务,只需先获取该反应器的任务队列,向任务队列添加线程,再获取该反应器的管道描述符,经过写入任意数据将该反应器唤醒便可。

服务器系统中多线程的运用

  在服务器系统中,咱们使用支持多线程的Reactor模式,并综合新链接建立和线程分配的业务场景,肯定了最后的服务器底层模型。

 

图3-14 多线程的Reactor模型

  如图3-14所示,系统中存在一个main Reactor负责监听accept链接。每当有新的链接产生时,反应器回调监听套接字处理器,并在其中建立一个任务,该任务是将这个新链接注册到某个指定的反应器中,并向该反应器发送唤醒事件。

  同时系统经过线程池管理多个工做反应器,工做反应器的数量是能够设置的,能够根据CPU的数目来肯定恰当的数量。每当监听Reactor中有新链接产生时,将会经过Round Robin轮询调度从线程池中选出一个工做反应器,做为新任务的发送对象。被选中的这个工做反应器也将会做为该链接的实际管理者,这个链接的全部操做都会在这个工做反应器所在线程中完成。

  经过以上设计,咱们的系统不但可以经过多线程充分利用到了多核CPU的性能,又经过固定线程数避免了系统整体处理能力不会随链接数增长而降低。同时因为一个链接彻底由一个线程管理,保证了对该链接的读写及事件处理的可以按照顺序执行,简化了多线程下实际业务逻辑的处理过程。

相关文章
相关标签/搜索