30 张图解 | 高频面试知识点总结:面试官问我高并发服务模型哪家强?

文章每周持续更新,原创不易,「三连」让更多人看到是对我最大的确定。能够微信搜索公众号「 后端技术学堂 」第一时间阅读(通常比博客早更新一到两篇)web

面试中常常会被问到高性能服务模型选择对比,以及如何提升服务性能和处理能力,这其中涉及操做系统软件和计算机硬件知识,其实都是在考察面试者的基础知识掌握程度,但若是没准备的话容易一头雾水,此次带你们从头至尾学习一遍,学完这一篇不再怕面试官刨根问底了!面试

任务类型

谈高并发服务模型选择以前,咱们先来看下程序的的任务类型,程序任务类型通常分为 CPU 密集型任务和 IO 密集型任务,这两种任务有各自的特色,对程序的要求是不同的须要分开对待。数据库

CPU密集型任务

一个程序任务大部分是计算类的,好比逻辑处理、数值比较和计算,咱们就称它是 CPU 密集型任务或计算密集型任务。CPU 密集型任务的特色是要进行大量的计算,消耗 CPU 资源,好比计算圆周率、视频编解码这些靠的是 CPU 的运算能力。编程

CPU 密集型任务虽然也能够用多任务完成,可是任务越多,任务之间切换的时间就越多,CPU 执行效率反而更低,因此要最高效地利用 CPU,任务并行数应当等于 CPU 的核心数,避免任务在 CPU 核之间频繁切换。windows

芯片线路
芯片线路

IO密集型任务

一个程序涉及到大量网络、磁盘等比较耗时的输入输出任务,就称它是 IO 密集型任务,这类任务的特色是 CPU 消耗不多,任务的大部分时间都在等待 IO 操做完成(由于 IO 的速度远远低于 CPU 和内存的速度,不是一个数量级的)。后端

对于 IO 密集型任务,任务越多 CPU 效率越高,但也不是无限的开启多任务,若是任务过多频繁切换的开销也不可忽视。常见的大部分程序都是执行 IO 密集型任务,好比互联网业务的 Web 服务,数据库操做等。api

五彩的以太网口
五彩的以太网口

服务模型

无论是 CPU 密集型任务仍是 IO 密集型任务,要提升服务器处理能力,能够从软件和硬件两个层面来作文章。缓存

先说软件层面,单个任务处理能力有限,能够经过启动多个功能彻底相同的服务实例,借此来提升服务总体处理性能,多服务实例的实现主流的技术有三种:多进程、多线程、多协程。固然除了用多实例的方式,还有 IO 多路复用、异步 IO 等技术,为了文章主题明确,不在本文展开讨论。服务器

服务模型哪家强

既然有三种技术实现,那么你可能会问,在三个模型里选一个最好的来实现服务,该如何选择一个适合的服务模型呢?微信

抱歉,小孩子才作选择我全都要!哈哈,开个玩笑。

答案是没有最好,服务模型选择要结合自身服务处理的任务类型。任务类型就是咱们上面说的 CPU 密集型和 IO 密集型,只有清楚的知道所处理业务的任务类型,才能在上述服务模型中选择其一或多种模型组合,来搭建适合你的高性能服务框架。

多进程服务模型

进程概念

程序是一些保存在磁盘上的指令的有序集合,是静态的。进程是程序执行的过程,包括了动态建立、调度和消亡的整个过程,进程是程序资源管理的最小单位

多进程模型

多进程模型是启动多个服务进程。原来由一个进程作的事,当一个进程忙不过来,建立几个功能同样的进程来帮它一块儿干活,人多力量大。

因为多进程地址空间不一样,数据不能共享,一个进程内建立的变量在另外一个进程是没法访问。操做系统看不下去了,凭什么同在一台机器,彼此相爱的两个进程不能说说话呢?

因而操做系统提供了各类系统调用,搭建起各个进程间通讯的桥梁,这些方法统称为进程间通讯 IPC (IPC InterProcess Communication)

常见进程间通讯方式

管道 Pipe

管道的实质是一个内核缓冲区,进程以先进先出 FIFO 的方式从缓冲区存取数据。 是一种半双工的通讯方式,数据只能单向流动,并且只能在具备亲缘关系(父子进程间)的进程间通讯。

管道工做原理

  1. 管道一端的进程顺序的将数据写入缓冲区,另外一端的进程则顺序的读出数据。

  2. 缓冲区能够看作是一个循环队列,一个数据只能被读一次,读出来后在缓冲区就不复存在了。

  3. 当缓冲区为读空或写满,读数据的进程或写数据进程进入等待队列。

  4. 空的缓冲区有新数据写入,或者满的缓冲区有数据读出时,唤醒等待队列中的进程继续读写。

管道图解
管道图解

命名管道 FIFO

上面介绍的管道也称为匿名管道,只能用于亲缘关系的进程间通讯。为了克服这个缺点,出现了有名管道 FIFO 。有名管道提供了一个路径名与之关联,以文件形式存在于文件系统中,这样即便不存在亲缘关系的进程,只要能够访问该路径也能相互通讯。

命名管道支持同一台计算机的不一样进程之间,可靠的、单向或双向的数据通讯。 FIFO图解.png

信号 Signal

信号是Linux系统中用于进程间互相通讯或者操做的一种机制,信号能够在任什么时候候发给某一进程,无需知道该进程的状态。若是该进程当前不是执行态,内核会暂时保存信号,当进程恢复执行后传递给它。

若是一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

信号在用户空间进程和内核之间直接交互,内核能够利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入 Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其余进程调用 kill 函数、软件异常产生信号。

消息队列 Message Queue

消息队列是存放在内核中的消息链表,每一个消息队列由消息队列标识符表示, 只有在内核重启或主动删除时,该消息队列才会被删除。

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 另外,某个进程往一个消息队列写入消息以前,并不须要另外读进程在该队列上等待消息的到达。 消息队列图解

共享内存 Shared memory

共享内存是一个进程把地址空间的一段,映射到能被其余进程所访问的内存,一个进程建立、多个进程可访问,进程就能够直接读写这一块内存而不须要进行数据的拷贝,从而大大提升效率。

共享内存使得多个进程能够能够直接读写同一块内存空间,是最快的可用 IPC 形式,是针对其余通讯机制运行效率较低而设计的。共享内存每每与其余通讯机制,如信号量配合使用,来实现进程间的同步和互斥通讯。

共享内存
共享内存

套接字 Socket

套接字你可能没听过这个名字,但绝对是接触的最多的一种进程间通讯方式。由于咱们熟悉的 TCP/IP 协议栈,也是创建在 socket 通讯之上,TCP/IP 构建起了当前的互联网通讯网络。

它是一种通讯机制,凭借这种机制,既能够在本机进程间通讯,也能够跨网络经过,由于,套接字经过网络接口将数据发送到本机的不一样进程或远程计算机的进程。

socket套接字
socket套接字

多线程服务模型

线程概念

线程是操做操做系统可以进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运做单位,一个进程内能够包含多个线程,线程是资源调度的最小单位。 进程线程关系

多线程模型

启动多个相同功能的进程能提升服务处理能力,但因为各个进程的地址空间相互隔离,通讯不便。

因而,多线程服务模型出场。经过前面的学习咱们知道,一个进程内的多个线程能够共享进程的所有系统资源。进程内建立的多个线程均可以访问进程内的全局变量。

固然没有免费的午饭,线程虽然能方便的访问进程资源,但也带来了额外的问题。好比多线程访公共资源带来的同步与互斥问题,不一样线程访问资源的前后顺序会相互影响,若是不作好同步和互斥会产生预期以外的结果,甚至死锁。

什么是多线程同步

多线程同步是线程之间的一种直接制约关系,一个线程的执行依赖另外一个线程的通知,当它没有获得另外一个线程的通知时必须等待,直到消息到达时才被唤醒,即有很强的执行前后关系。

好比你搭建了一个商城服务。这个服务的下单流程是这样的:第一步必需要先挑选商品加入购物车,第二步才能结帐计算订单金额,假设这两个步骤的操做分别由两个线程去完成,则这两个线程的操做顺序很重要,必须是先下单再结帐,这就是线程同步。 购物车

什么是多线程互斥

多线程互斥指的是多线程对资源访问的排他性。所谓排他性,就是当有多个线程都要使用某一共享资源时,任什么时候刻最多只容许一个线程得到对这个共享资源的使用权,当共享资源被其中一个线程占有时,其余未得到资源的线程必须等待,直到占用资源的线程释放资源。

打个比方,大家班只有一台投影仪,当一个同窗在上面放电影的时候,若是老师进来上课要用这个投影仪,那就只能由这个同窗放弃投影仪的使用权,交给老师上课投影使用,对,教室里惟一的投影仪是共享资源,具备排他性,老师和学生比做是两个线程的话,那这两个线程是互斥的访问共享资源(投影仪)。

投影仪
投影仪

多线程同步和互斥方法

Linux 系统提供如下几种方法来解决多线程的同步和互斥问题,分别是:互斥锁、条件变量、读写锁、自旋锁、条件变量。

互斥锁(同步)

互斥锁的做用是对临界区加以保护,以使任意时刻只有一个线程可以执行临界区的代码,实现了多线程对临界资源的互斥访问。

互斥锁接口函数:

互斥锁api
互斥锁api

条件变量(同步)

条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊状况发生为止。适合多个线程等待某个条件的发生,不使用条件变量,那么每一个线程就不断尝试互斥锁并检测条件是否发生,浪费系统资源

一般条件变量和互斥锁同时使用。条件的检测是在互斥锁的保护下进行的。若是一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。若是另外一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,从新得到互斥锁,从新评价条件,能够用来实现线程间的同步。

条件变量系统 API 以下:

条件变量API
条件变量API

读写锁(同步)

互斥量要么是加锁状态,要么是不加锁状态,并且一次只有一个线程对其进行加锁。读写锁能够有3种状态:读加锁状态、写加锁状态和不加锁状态

一次只有一个线程能够占有写模式读写锁,可是能够有多个线程同时占有读模式的读写锁。所以,读写锁适合于对数据结构的读次数比写次数多得多的状况,且读写锁比互斥量具备更高的并行性。

读写锁加锁规则

1:若是某线程申请了读锁,其它线程能够再申请读锁,但不能申请写锁;

2:若是某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

读写锁系统 API

读写锁API
读写锁API

自旋锁(同步)

互斥锁得不到锁时,线程会进入休眠,引起任务上下文切换,任务切换涉及一系列耗时的操做,所以用互斥锁一旦遇到阻塞切换代价是十分昂贵的。

而自旋锁阻塞后不会引起上下文切换,当锁被其余线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态,直到获得锁,所谓的自旋就是循环等待的意思。

自旋锁在用户态使用的比较少,在内核使用的比较多。自旋锁适用于临界区代码比较短,锁的持有时间比较短的场景,不然会让其余线程一直等待形成饥饿现象。

自旋锁 API 接口

自旋锁API
自旋锁API

信号量(同步与互斥)

信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

信号量是一个特殊类型的变量,它能够被增长或者减小。可根据操做信号量值的结果判断是否对公共资源具备访问的权限,当信号量值大于 0 时,则能够访问,不然将阻塞。但对其的访问被保证是原子操做,即便在一个多线程程序中也是如此。

信号量类型:

  • 二进制信号量,它只有0和1两种取值。适用于临界代码每次只能被一个执行线程运行,就要用到二进制信号量。

  • 计数信号量。它能够有更大的取值范围,适用于临界代码容许有限数目的线程执行,就须要用到计数信号量。

信号量 API

信号量API
信号量API

协程服务模型

什么是协程

什么是协程呢?协程 Coroutines 是一种比线程更加轻量级的微线程。类比一个进程能够拥有多个线程,一个线程也能够拥有多个协程,所以协程又称微线程和纤程。

协程图解
协程图解

能够粗略的把协程理解成子程序调用,每一个子程序均可以在一个单独的协程内执行。

协程子程序模型
协程子程序模型

协程服务模型

为了说明什么是协程模型,先用多线程下的生产者消费者模型举个栗子。

启动两个线程分别执行两个函数 Do_some_IODo_some_process ,第一个作耗时的 IO 处理操做,第二个对 IO 操做结果作快速的处理计算工做。伪代码以下:

函数伪代码
函数伪代码

多线程执行过程是这样的:

  1. 生产者线程先调用函数 Do_some_IO 作比较耗时的 IO 操做,好比从网络套接字中读取数据这类操做。

  2. 在生产者线程执行 Do_some_IO 完成数据读取以前,消费者线程要阻塞等待。

  3. 在消费者线程执行 Do_some_process 完成数据处理完成以前,生产者线程要阻塞等待。

  4. 在消费者线程执行 Do_some_process 完成数据处理完成以后,要通知生成者线程继续 Do_some_IO
    多线程执行模型.png

能够看到,多线程模型为了保证各个线程并行工做,须要额外作不少线程间的同步和通知工做,并且线程频繁的在阻塞和唤醒间切换,咱们知道 Linux 下线程是轻量级线程 LWP ,每次线程切换涉及用户态和内核态的切换,仍是很消耗性能的。

一样的场景在协程模型里是怎么处理的呢?仍是用前面的例子,说明协程模型的执行流程。

Do_some_IO()       // IO处理协程
Do_some_process()  // 计算处理协程
  1. 分配生产者协程执行 Do_some_IO 作 IO 处理操做,分配消费者协程执行 Do_some_process 计算处理操做。
  2. 在生产者协程工做期间,消费者协程保持等待。
  3. 当生产者协程完成 IO 处理,返回处理结果给消费者,并把程序执行权限交给消费者协程向下执行。
协程执行时间线.png
协程执行时间线.png

协程优点

  • 因为协程在线程内实现,所以始终都是一个线程操做共享资源,因此不存在多线程抢占资源和资源同步问题。

  • 生产者协程和消费者协程,互相配合协做完成工做,而不是相互抢占,并且协程建立和切换的开销比线程小得多。

硬件提高性能

前面讲的多线程、多进程、协程都还只是软件层面的提升服务处理能力。真正硬核的是从硬件层面提升处理能力,增长 CPU 物理核心数目,固然硬件都是有成本的,因此只有软件层面已经充分榨干性能才会考虑增长硬件。

不过,老板有钱买最好最贵的服务器另说,这是人民币玩家和穷逼玩家的区别了,软件工程师留下了贫困的泪水。

增长机器核心数

CPU领域有一条摩尔定律:大概 18 个月会将芯片的性能提升一倍。如今这个定律变的愈来愈难以突破,CPU 晶体管密度工做频率很难再提升,转而经过增长 CPU 核心数目的方式提升处理器性能。

cpu
cpu

目前商用服务器架构基本都是多核处理器,多核的处理器可以真正作到程序并行运行,处理效率大幅度提高,那该如何查看 CPU 核心数目呢?

对于 Windows 操做系统,打开任务管理器,经过界面的「内核」和「逻辑处理器」能看到。

windows 查看核心
windows 查看核心

查看 cpu 核心数

对于 Linux 操做系统,经过下面 2 种方式查看 CPU 核心相关信息。

1. 经过cpuinfo文件查看

使用cat /proc/cpuinfo查看 cpu 核心信息,以下两个信息:

  • processor,指明第几个cpu处理器
  • cpu cores,指明每一个处理器的核心数

cpuinfo 输出示例:

cpuinfo
cpuinfo

2. 经过编程接口查看

除了上面以文件的形式查看 cpu 核心信息以外,系统还提供了编程接口能够查询,系统 API 以下。

查看核数API
查看核数API

CPU亲和性

CPU 亲和性是绑定某一进程或线程到特定的 CPU 或 CPU 集合,从而使得该进程或线程只能被调度运行在绑定的 CPU或 CPU 集合上。

为何要设置 CPU 亲和性绑定 CPU 呢?理论上进程上一次运行后的上下文信息会保留在 CPU 的缓存中,若是下一次仍然将该进程调度到同一个 CPU 上,就能避免缓存未命中对 CPU 处理性能的影响,从而使得进程的运行更加高效。

假如某些进程或线程是 CPU 密集型的,不但愿被频繁调度,又或者你有其余特殊需求,不但愿进程或线程被调度在不一样 CPU 之间频繁切换,则能够将该进程或线程绑定到特定的 CPU 上 ,能够在特定场景下优化程序性能。

绑定进程

在多进程模型中,绑定进程到特定的核心,下面是绑定进程的系统 API 设置进程亲和性

绑定线程

在多线程模型中,绑定线程到特定的核心,下面是绑定线程的系统 API

设置线程亲和性
设置线程亲和性

总总结结

本文从程序任务类型出发,区分任务为 CPU 密集型和 IO 密集型两大类。接着分别说明提升基于这两类任务的服务性能方法,分为软件层面的方法和硬件层面的方法,其中软件层面主要讲述利用多进程、多线程以及协程模型,固然现有的技术还有 IO 多路复用、异步 IO 、池化技术等方案。讲到多线程和多进程,顺势说明了进程间通讯和线程间同步互斥技术。

第二部分,讲解了从硬件层面提升服务性能:提升机器核心数,并教你如何查看 CPU 核心数的方法。最后,还能够经过软硬结合的方式,把硬件核心绑定到指定进程或者线程执行,最大程度的利用 CPU 性能。

但愿经过本文的学习,读者对高性能服务模型有个初步的了解,并能对服务优化的方法和利弊举例一二,就是本文的价值所在。

再聊两句(求个三连)

感谢各位的阅读,文章的目的是分享对知识的理解,技术类文章我都会反复求证以求最大程度保证准确性,若文中出现明显纰漏也欢迎指出,咱们一块儿在探讨中学习。

若是以为文章写的还行,对你有所帮助,不要白票 lemon,动动手指「点赞」「三连」是对我持续创做的最大支持。

今天的技术分享就到这里,咱们下期再见。

能够微信搜索公众号「 后端技术学堂 」回复「资料」「1024」有我给你准备的各类编程学习资料。文章每周持续更新,咱们下期见!

公众号二维码.png

相关文章
相关标签/搜索