【muduo】多线程服务器的适用场合与编程模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daaikuaichuan/article/details/85414641

一、进程与线程

1、进程的概念

  直观来说,一个进程是”内存中正在运行的程序”。每个进程都有自己独立的地址空间。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位。进程切换时,耗费资源较大,效率要差一些

2、关于进程的一个形象比喻(人)

  • 每个人都有自己的记忆(memory) 。

  • 人与人通过谈话来交流(消息传递) 。

  • 谈话可以是面谈(同一个服务器),也可以在电话里谈(不同的服务器,有网络通信) 。

  • 面谈和电话谈的区别是,面谈可以知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。

【有了这些比喻,分布式系统一些术语,可以这样解释: 】

  • 容错: 万一有人死了

  • 扩容:新人中途加进来

  • 负载均衡:把甲的活儿挪给乙做

  • 退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启等等各种场景,十分便利

3、线程的概念

  线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)和分配执行的最小单位。线程的特点是共享地址空间,从而可以高效的共享数据。多线程的价值是为了更好的发挥多核处理器的效能。线程切换时,耗费资源较小,效率要高一些


二、多进程和多线程的适用场景

在这里插入图片描述

1、需要频繁创建销毁的优先用线程

  这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的。

2、需要进行大量计算的优先使用线程

  所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。

3、强相关的处理用线程,弱相关的处理用进程

  什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

  一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

  当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

4、可能要扩展到多机分布的用进程,多核分布的用线程

5、都满足需求的情况下,用你最熟悉、最拿手的方式


三、两个服务器的重要性能指标:吞吐量和延迟

  吞吐量(Throughput)和延迟(Latency)是衡量软件系统的最常见的两个指标。

  延迟一般包括单向延迟(One-way Latency)和往返延迟(Round Trip Latency),实际测量时一般取往返延迟。它的单位一般是ms、s、min、h等。而吞吐量一般指相当一段时间内测量出来的系统单位时间处理的任务数或事务数(TPS)。注意“相当一段时间”,不是几秒,而可能是十几分钟、半个小时、一天、几周甚至几月。它的单位一般是TPS、每单位时间写入磁盘的字节数等。

  从业务角度看,吞吐量可以用:请求数/秒、页面数/秒、人数/天或处理业务数/小时等单位来衡量;从网络角度看,吞吐量可以用:字节/秒来衡量。

  根据延迟和吞吐量我们还可以计算并发度(Concurrency),公式如下:并发度 = 吞吐量 * 延迟。比如一个任务的处理花费1ms,吞吐量为1000tps,那么并发度就等于1/1000*1000=1,可以得出任务处理线程模型是单线程模型。

【关于吞吐量和延迟的一个形象比喻】:

  很多人都曾经认为大的吞吐量就意味着低延迟,高延迟就意味着吞吐量变小。下面的比喻可以解释这种观点根本不对。

  我们可以把网络发送数据包比喻成去街边的 ATM 取钱。每一个人从开始使用 ATM 到取钱结束整个过程都需要一分钟,所以这里的延迟是60秒,那吞吐量呢?当然是 1/60 人/秒。现在银行升级了他们的 ATM 机操作系统,每个人只要30秒就可以完成取款了!延迟是 30秒,吞吐量是 1/30 人/秒。很好理解,可是前面的问题依然存在对不对?别慌,看下面。

  因为这附近来取钱的人比较多,现在银行决定在这里增加一台 ATM 机,一共有两台 ATM 机了。现在,一分钟可以让4个人完成取钱了,虽然你去排队取钱时在 ATM 机前还是要用 30 秒!也就是说,延迟没有变,但吞吐量增大了!可见,吞吐量可以不用通过减小延迟来提高。

  好了,现在银行为了改进服务又做出了一个新的决定:每个来取钱的客户在取完钱之后必须在旁边填写一个调查问卷,用时也是30秒。那么,现在你去取钱的话从开始使用 ATM 到完成调查问卷离开的时间又是 60 秒了!换句话说,延迟是60秒。而吞吐量根本没变!一分钟之内还是可以进来4个人!可见,延迟增加了,而吞吐量没有变。

  从这个比喻中我们可以看出,延迟测量的是每个客户(每个应用程序感受到的时间长短,而吞吐量测量的是整个银行(整个操作系统的处理效率,是两个完全不同的概念。


四、CPU-bound(计算密集型) 和I/O bound(I/O密集型)

1、计算密集型 (CPU-bound)

  在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

  CPU bound的程序主要是执行计算任务,响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

计算密集型的较理想线程数 = CPU内核线程数 * 2

2、I/O密集型 (IO-bound)

  I/O bound 指的是系统的CPU效能相对硬盘/内存的效能要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写,此时 CPU Loading 不高。

  I/O bound的程序主要是进行I/O操作,执行I/O操作的时间较长,这是CPU出于空闲状态,导致CPU的利用率不高

  比如说一个后台管理系统,我们后端的接口都是用来做CURD(增删改查)的,这个时候就会涉及到网络传输,IO交互,造成的结果就是IO等待,线程等待。因为线程上下文切换也是有代价的,这个时候面临的问题就是如何设置线程池的合理大小,使得CPU尽量不处于空闲状态(线程等待是不占CPU的),尽量提高CPU利用率。下面是一个对于IO密集型应用线程池设置的公式:

I/O密集型的较理想线程数 = CPU核心数 / 密集计算所占比重

【Note】:
  计算密集型程序适合C/C++语言多线程,I/O密集型适合脚本语言开发的多线程。


五、Reactor模型和Proactor模型

  Linux中有五种I/O模型,其中前四种:阻塞模型、非阻塞模型、信号驱动模型、I/O复用模型都是同步模型;还有一种是异步模型。

1、Reactor模型

  Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。

  Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。
在这里插入图片描述
在这里插入图片描述

(1)Reactor模型的组成部分

  • 描述符(handle):由操作系统提供的资源,用于识别每一个事件,如Socket描述符、文件描述符、信号的值等。在Linux中,它用一个整数来表示。事件可以来自外部,如来自客户端的连接请求、数据等。事件也可以来自内部,如信号、定时器事件。

  • 同步事件多路分离器(event demultiplexer)事件的到来是随机的、异步的,无法预知程序何时收到一个客户连接请求或收到一个信号。所以程序要循环等待并处理事件,这就是事件循环(event loop)在事件循环中,等待事件一般使用I/O复用技术实现。在linux系统上一般是select、poll、epol_waitl等系统调用,用来等待一个或多个事件的发生。I/O框架库一般将各种I/O复用系统调用封装成统一的接口,称为事件多路分离器。调用者会被阻塞,直到分离器分离的描述符集上有事件发生。

  • 事件处理器(event handler):I/O框架库提供的事件处理器通常是由一个或多个模板函数组成的接口。这些模板函数描述了和应用程序相关的对某个事件的操作,用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数(必须是非阻塞的)一般声明为虚函数,以支持用户拓展。

  • 具体的事件处理器(concrete event handler):是事件处理器接口的实现。它实现了应用程序提供的某个服务。每个具体的事件处理器总和一个描述符相关。它使用描述符来识别事件、识别应用程序提供的服务。

  • Reactor 管理器(reactor):定义了一些接口,用于应用程序控制事件调度,以及应用程序注册、删除事件处理器和相关的描述符。它是事件处理器的调度核心。 Reactor管理器使用同步事件分离器来等待事件的发生。一旦事件发生,Reactor管理器先是分离每个事件,然后调度事件处理器,最后调用相关的模板函数来处理这个事件。
    在这里插入图片描述
      对于Reactor模式,可以将其看做由两部分组成,一部分是由Boss组成,另一部分是由worker组成。Boss就像老板一样,主要是拉活儿、谈项目,一旦Boss接到活儿了,就下发给下面的work去处理。也可以看做是项目经理和程序员之间的关系。

(2)Reactor模型的应用场景

  对于高并发系统,常会使用Reactor模式,其代替了常用的多线程处理方式,节省系统的资源,提高系统的吞吐量。下面用比较直观的形式来介绍这种模式的使用场景。

  以餐厅为例,每一个人就餐就是一个事件,顾客会先看下菜单,然后点餐,处理这些就餐事件需要服务人员。就像一个网络服务会有很多的请求,服务器会收到每个请求,然后指派工作线程去处理一样。

  在多线程处理方式下:

  • 一个人来就餐,一个服务员去服务,然后客人会看菜单,点菜。 服务员将菜单给后厨。
  • 二个人来就餐,二个服务员去服务……
  • 五个人来就餐,五个服务员去服务……

  这类似多线程的处理方式,一个事件到来,就会有一个线程为其服务。很显然这种方式在人少的情况下会有很好的用户体验,每个客人都感觉自己享有了最好的服务。如果这家餐厅一直这样同一时间最多来5个客人,这家餐厅是可以很好的服务下去的。

  由于这家店的服务好,吃饭的人多了起来。同一时间会来10个客人,老板很开心,但是只有5个服务员,这样就不能一对一服务了,有些客人就不能马上享有服务员为其服务了。老板为了挣钱,不得不又请了5个服务员。现在又好了,每位顾客都享受最好最快的待遇了。

  越来越多的人对这家餐厅满意,客源又多了,同时来吃饭的人到了20人,老板高兴但又高兴不起来了,再请服务员吧,占地方不说,还要开工钱,再请人就挣不到到钱了。

  怎么办呢?老板想了想,10个服务员对付20个客人也是能对付过来的,服务员勤快点就好了,伺候完一个客人马上伺候另外一个,还是来得及的。综合考虑了一下,老板决定就使用10个服务人员的线程池!

  但是这样又有一个比较严重的缺点:如果正在接受服务员服务的客人点菜很慢,其他的客人可能就要等好长时间了。有些脾气火爆的客人可能就等不了走人了。

  这样,我么那就引入了Reactor模式,那么,Reactor模式是如何处理这个问题呢?

  老板后来发现,客人点菜比较慢,大部服务员都在等着客人点菜,其实干的活不是太多。老板之所以能当老板当然有点不一样的地方,终于发现了一个新的方法,那就是:当客人点菜的时候,服务员就可以去招呼其他客人了,等客人点好了菜,直接招呼一声“服务员”,马上就有个服务员过去服务。在用了这个新方法后,老板进行了一次裁员,只留了一个服务员!这就是用单个线程来做多线程的事。实际的餐馆都是用的Reactor模式在服务。

(3)Reactor的几种经典模式

【reactor + thread pool】:

在这里插入图片描述

【multiple reactors】:

在这里插入图片描述

【multiple reactors + thread pool(one loop per thread + threadpool)】:

在这里插入图片描述

2、Proacotr模型

  Proactor是和异步I/O相关的。

  在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离器就把这个事件传给事先注册的处理器(事件处理函数或者回调函数),由后者来做实际的读写操作。

  在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。

在这里插入图片描述

  Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备。


六、多线程服务器的常用编程模型

  • 每请求创建一个线程, 使用阻塞式IO操作; 可惜伸缩不佳。

  • 使用线程池, 同样使用阻塞式IO操作, 这是提高性能的措施

  • 使用non-blocking IO + IO multiplexing

  • Leader/Follower等。

1、one loop per thread

  在这个模型下,程序里每个IO线程有一个event loop(或者叫Reactor),用于处理读写和定时事件。也就是将事件处理器设置成线程池,每个线程对应一个事件处理器(event loop);因为事件处理器主要处理的是I/O事件,而且每个事件处理器可能会处理一个连接上的多个I/O事件,而不是处理完一个事件后直接断开,因此muduo选择每个事件处理器一个event loop。这样,连接建立后,对于这条连接上的所有事件全权由它的事件处理器在event loop中处理。

【这种方式的好处】:

  • 线程数目基本固定, 可以在程序启动的时候设置, 不会频繁创建与销毁。

  • 可以很方便地在线程间调配负载。

  • IO事件发生的线程是固定的, 同一个TCP链接不必考虑事件并发。

2、推荐模式(one loop per thread + thread pool)

  推荐的C++多线程服务器模式为: one(event) loop per thread + thread pool

  event loop(也叫IO loop)用作IO multiplexing, 配合non-blocking IO和定时器。

  thread pool用来做计算, 具体可以是任务队列或是生产者消费者队列。


七、进程间通信只用TCP

  Linux下进程通信方式有:匿名管道(pipe)、命名管道(FIFO)、消息队列、共享内存、信号、Socket。TCP的好处在于:

  • 可以跨主机,具有伸缩性 。

  • TCP port由一个进程独占,且操作系统会自动回收(listening port和已建立连接的TCP socket都是文件描述符,在进程结束时,操作系统会关闭所有文件描述符) 。

  • TCP还能跨语言,服务端和客户端不必使用同一种语言 。

  • TCP连接是可再生的,连接的任何一方都可以退出再启动,重建连接之后就能继续工作,这对开发牢靠的分布式系统意义重大。


八、多线程服务器的适用场合

  开发服务器端程序的一个基本任务是处理并发连接, 现在服务端网络编程处理并发连接主要有两种方式:

  • 当线程很廉价时, 一台机器上可以创建远高于CPU数目的线程。

  • 当线程很宝贵时, 一台机器上只能创建与CPU数目相当的线程。

1、必须用单线程的场合

  • 程序可能fork。

  • 限制程序的CPU占用率。

【一个程序fork之后, 一般有两种行为】:

  • 立刻执行exec(), 变身为另一个程序(负责启动job的守护进程)。

  • 不调用exec(), 继续运行当前程序。

    • 要么通过共享的文件描述符与父进程通信, 协同完成任务。

    • 要么接过父进程传来的文件描述符, 完成独立的任务

2、单线程程序的优缺点

  • 单线程程序的优势: 简单, 一个基于IO multiplexing的event loop。

  • 用很少的CPU负载就能让IO跑满, 或者用很少的IO流量就能让CPU跑满, 那么多线程就没有啥用处。

3、适用多线程程序的场景

  多线程的适用场景:提高响应速度, 让IO和计算互相重叠, 降低latency(延迟),虽然多线程不能提高绝对性能, 但多线程能提高平均响应性能。一个程序要想做多线程, 大致要满足:

  • 多个CPU可用;(单核机器上多线程没有性能优势, 或许能简化并发业务逻辑的实现)。
  • 线程间有共享数据, 即内存中的全局状态, 如果没有共享数据, 用运行多个单线程的进程就行。
  • 提供非均质的服务; 事件的响应有优先级的差异, 用专门的线程来处理优先级高的事件, 防止优先级反转。
  • latency和throughout同样重要, 不是逻辑简单的IO bound或CPU bound程序(程序有相当的计算量)。
  • 利用异步操作。
  • 能scale up, 一个好的线程程序能享受增加CPU数目带来的好处。
  • 具有可预测的性能, 线程数一般不随负载变化。
  • 多线程能有效地划分责任和功能, 让每个线程的逻辑比较简单, 任务单一, 便于编码。

九、多线程服务器的适用场合

1、Linux能同时启动多少个线程?

  对于 32-bit Linux,一个进程的地址空间是 4G,其中用户态能访问 3G 左右,而一个线程的默认栈 (stack) 大小是 8M,心算可知,一个进程大约最多能同时启动 350 个线程左右。

2、多线程能提高并发度吗?

  如果指的是“并发连接数”,不能。假如单纯采用 thread per connection 的模型,那么并发连接数大约350,这远远低于基于事件的单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 IO multiplexing event loop 的编程模型,又称 Reactor 模式。单个的event loop处理1万个并发长连接并不稀罕, 一个multi-loop的多线程程序应该能轻松支持5万并发连接。

3、多线程能提高吞吐量吗?

  对于计算密集型服务, 不能。为了在并发请求数很高时也能保持稳定额吞吐量, 我们可以用线程池, 线程池的大小应该满足"阻抗匹配原则"。

4、多线程能降低响应时间么?

  如果设计合理, 充分利用多核资源的话, 多线程可以降低响应时间, 在突发(burst)请求时效果尤为明显;

5、多线程程序如何让IO和计算互相重叠, 降低latency(延迟)?

  把IO操作(通常是写操作)通过BlockingQueue交给别的线程去做, 自己不必等待;

6、什么是线程池大小的阻抗匹配原则?

  如果池中执行任务时,密集计算所占时间比重为P(0<P<=1),而系统一共有C个CPU,为了让C个CPU跑满而不过载,线程池大小的经验公式T=C/P,即T*P=C(让CPU刚好跑满 )

  • 假设C=8,P=1.0,线程池的任务完全密集计算,只要8个活动线程就能让CPU饱和。

  • 假设C=8,P=0.5,线程池的任务有一半是计算,一半是IO,那么T=16,也就是16个“50%繁忙的线程”能让8个CPU忙个不停。

参考:https://blog.csdn.net/RUN32875094/article/details/79515384
https://blog.csdn.net/adparking/article/details/37583965
https://www.cnblogs.com/binyao/p/5162424.html
https://blog.csdn.net/q_l_s/article/details/51538039
https://blog.csdn.net/u013246898/article/details/52044091
https://blog.csdn.net/u013074465/article/details/46276967

猜你喜欢

转载自blog.csdn.net/daaikuaichuan/article/details/85414641