【面试技巧】CPU密集型任务类型谈高并发服务模型选择之前

作者 |柠檬编码器

在采访中,人们经常被问到高性能服务模型的选择和比较,以及如何提高服务性能和处理能力。这涉及操作系统软件和计算机硬件知识。其实就是考查考生的基础知识。但是如果没有做好准备,就很容易混淆。这一次,我将带你从头到尾学习。学完这篇文章,你再也不怕面试官的问话了!

任务类型

在谈高并发服务模型的选择之前,我们先来看看程序的任务类型。程序任务类型一般分为CPU密集型任务和IO密集型任务。这两个任务各有特点。要求不同,需要分别对待。

CPU 密集型任务

程序任务主要是计算性的,例如逻辑处理、数值比较和计算,我们称之为CPU密集型任务或计算密集型任务。 CPU密集型任务的特点是需要进行大量的计算,消耗CPU资源,比如计算pi和视频编解码,这些都依赖于CPU的计算能力。

虽然CPU密集型任务也可以通过多任务来完成,但是任务越多,任务之间切换的时间越长,CPU执行效率就越低,所以要最高效地利用CPU,数量并行任务的数量应等于 CPU 的内核数,以避免 CPU 内核之间频繁切换任务。

IO 密集型任务

涉及大量网络、磁盘等耗时输入输出任务的程序称为IO密集型任务。这类任务的特点是 CPU 消耗低,任务大部分时间都在等待 IO 操作。完成(因为IO的速度远低于CPU和内存的速度,不是一个数量级)。

对于IO密集型任务,任务越多CPU效率越高,但不是无限多任务。如果任务太多,频繁切换的开销也不容忽视。大多数常见的程序都执行IO密集型任务,例如互联网业务的Web服务、数据库操作等。

服务模式

无论是CPU密集型任务还是IO密集型任务,要提升服务器的处理能力,可以从软件和硬件两个层面做文章。

我们先说软件层面。单个任务的处理能力是有限的。您可以启动多个具有相同功能的服务实例,以提高服务的整体处理性能。实现多服务实例的主流技术有3种:多进程、多线程、多协程。当然,除了多实例的方式,还有IO复用、异步IO等技术。为了明确文章的主题,本文不再讨论。

既然有三种技术实现,你可能会问,在三种模型中选择最好的一种来实现服务,如何选择合适的服务模型?

对不起,只有孩子才能做出选择,我要全部!哈哈,开个玩笑。

答案是没有最好的,服务模型结合自己的服务选择要处理的任务类型。任务类型就是我们上面提到的CPU密集型和IO密集型。只有清楚地了解自己正在处理的业务的任务类型,才能选择上述一种或多种服务模型来构建适合自己的高性能服务框架。 .

多进程服务模型

过程概念

程序是存储在磁盘上的指令的有序集合,并且是静态的。进程是程序执行的过程,包括动态创建、调度和消亡的全过程。进程是程序资源管理的最小单位。

多进程模型

多进程模型是启动多个服务进程。结果证明是通过一个过程完成的。当一个进程太忙时,会创建几个具有相同功能的进程来帮助它协同工作。人数更强大。

由于多个进程的地址空间不同,数据不能共享,在一个进程中创建的变量不能在另一个进程中访问。操作系统已经受不了了。为什么同一台机器上相爱的两个进程不能说话?

操作系统提供各种系统调用来搭建进程间通信的桥梁。这些方法统称为IPC(IPC InterProcess Communication)

常见的进程间通信方式

管子

管道的本质是内核缓冲区,进程以先进先出FIFO的方式从缓冲区中访问数据。它是一种半双工通信方式。数据只能向一个方向流动,并且只能在具有亲和性的进程之间(父子进程之间)进行通信。

管道的工作原理

管道一端的进程顺序将数据写入缓冲区,另一端的进程顺序读取数据。缓冲区可以看成是一个循环队列,一个数据只能被读取一次,读取后就不再存在于缓冲区中。当缓冲区为空或满时,读数据的进程或写数据的进程进入等待队列。当空缓冲区有新数据写入,或者满缓冲区有数据读取时,唤醒等待队列中的进程继续读写。

管道图

命名管道先进先出

上述管道也称为匿名管道,只能用于亲属关系的进程间通信。为了克服这个缺点,出现了著名的管道 FIFO。命名管道提供了一个与之关联的路径名,并以文件的形式存在于文件系统中,这样即使进程之间没有关系,只要该路径可以访问,它们就可以相互通信其他。

命名管道支持同一台计算机上不同进程之间可靠的单向或双向数据通信。

信号信号

信号是Linux系统中用于进程间通信或操作的一种机制。在不知道进程状态的情况下,可以随时向进程发送信号。如果进程当前没有执行,内核会临时保存信号,并在进程恢复执行时将其传递给进程。

如果信号被进程设置为阻塞,则信号的传递会延迟,直到它的阻塞被取消,然后再传递给进程。

信号直接在用户空间进程和内核之间进行交互。内核可以使用信号来通知用户空间进程发生了哪些系统事件。信号事件主要来自两个来源:

硬件来源:用户按键Ctrl+C退出,无效存储访问等硬件异常。软件终止:终止进程信号,其他进程调用kill函数,软件异常产生信号。消息队列消息队列

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

消息队列是消息的链表,存储在内核中并由消息队列标识符标识。消息队列克服了信令中信息量少、管道只能承载无格式字节流、缓冲区大小有限等缺点。此外,在一个进程将消息写入消息队列之前,不需要另一个读取进程等待消息到达队列。

共享内存共享内存

共享内存是将地址空间的一部分映射到其他进程可以访问的内存的进程。当一个进程被创建并且多个进程可以访问它时,该进程可以直接读写这块内存而不需要数据。复制,从而大大提高效率。

共享内存使多个进程可以直接读写同一个内存空间。它是可用的最快的 IPC 形式,专为其他通信机制的低效率而设计。共享内存常与信号量等其他通信机制配合使用,实现进程间的同步和互斥通信。

共享内存

套接字套接字

Sockets 这个名字你可能没有听说过,但它绝对是最常用的进程间通信方式。因为大家熟悉的 TCP/IP 协议栈也是建立在 socket 通信之上的,所以 TCP/IP 构建了现在的 Internet 通信网络。

它是一种通信机制,通过它可以在本地机器上的进程之间以及通过网络进行通信,因为套接字将数据发送到本地机器上的不同进程或通过网络接口进程发送到远程计算机。

插座插座

多线程服务模型

线程概念

线程是操作系统可以调度操作的最小单位。线程包含在进程中,是进程中的实际操作单元。一个进程可以包含多个线程,线程是资源调度的最小单位。

多线程模型

启动多个具有相同功能的进程可以提高服务处理能力,但由于每个进程的地址空间相互隔离,通信不便。

因此,多线程服务模型应运而生。通过前面的研究,我们知道一个进程中的多个线程可以共享该进程的所有系统资源。一个进程内创建的多个线程可以访问进程内的全局变量。

当然没有免费的午餐。虽然线程可以很容易地访问进程资源,但它们也带来了额外的问题。比如多线程访问公共资源导致的同步和互斥问题,不同线程访问资源的顺序会相互影响,如果同步和互斥做得不好,会出现意想不到的结果甚至死锁.

什么是多线程同步

多线程同步是线程之间的直接约束关系。一个线程的执行依赖于另一个线程的通知。当它没有被另一个线程通知时,它必须等到消息到达时被唤醒,即有很强的执行优先级关系。

例如,您构建了一个商城服务。该服务的下单流程如下:第一步,您必须选择要添加到购物车的产品,第二步,您可以结帐并计算订单金额。假设这两个步骤的操作是由两个线程完成的,那么这两个线程的操作顺序就很重要了。您必须先下订单,然后再结帐。这是线程同步。

什么是多线程互斥锁

多线程互斥是指多个线程对资源的独占访问。所谓排他性,是指当多个线程要使用共享资源时,在任何时候最多允许一个线程获得使用共享资源的权利。当共享资源被其中一个线程占用时,其他没有获得该资源的线程必须等到持有该资源的线程释放该资源。

例如,您的班级只有一台投影仪。同学在上面放电影的时候,如果老师进来使用投影仪,同学只能放弃使用投影仪的权利,交出投影仪。对于老师在课堂上使用投影,是的,教室里唯一的投影仪是共享资源,是专有的。如果把老师和学生比作两个线程,那么这两个线程对共享资源(投影仪)的访问是互斥的。

多线程同步和互斥方法

Linux系统提供了以下解决多线程同步和互斥问题的方法,即:互斥锁、条件变量、读写锁、自旋锁、条件变量。

互斥锁

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

互斥接口函数:

互斥 API

条件变量

条件变量用于等待,而不是锁定。条件变量用于自动阻塞线程,直到出现特殊情况。适合多线程等待某个条件的发生。如果不使用条件变量,则每个线程不断尝试互斥体并检测条件是否发生,浪费系统资源。

通常同时使用条件变量和互斥锁。条件的检测是在互斥锁的保护下进行的。如果条件为假,线程会自动阻塞并释放互斥锁以等待状态更改。如果另一个线程改变了条件,它会向关联的条件变量发出信号,唤醒一个或多个等待它的线程,重新获取互斥体,重新评估条件,并可用于实现线程间同步。

条件变量系统API如下:

条件变量 API

读写锁

互斥锁要么被锁定,要么被解锁,一次只有一个线程锁定它。读写锁可以有三种状态:读锁定状态、写锁定状态和解锁状态。

一次只有一个线程可以持有一个写模式读写锁,但多个线程可以同时持有一个读模式读写锁。因此,读写锁适用于对数据结构的读取次数远多于写入次数的情况,并且读写锁比互斥锁具有更高的并行度。

图片[1]-【面试技巧】CPU密集型任务类型谈高并发服务模型选择之前-老王博客

读写锁锁定规则

1:如果一个线程申请读锁,其他线程可以申请读锁,但不能申请写锁;

2:如果一个线程申请写锁,其他线程不能申请读锁,也不能申请写锁。

读写锁系统API

读写锁API

自旋锁

当互斥锁无法锁定时,线程会进入休眠状态,导致任务上下文切换。任务切换涉及一系列耗时的操作。因此,一旦遇到阻塞切换,使用互斥锁是非常昂贵的。

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

自旋锁在用户模式中使用较少,而在内核中使用较多。自旋锁适用于临界区的代码比较短,持有锁的时间比较短的场景,否则其他线程会等待导致饥饿。

自旋锁 API 接口

自旋锁 API

信号量

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

信号量是一种特殊类型的变量,可以递增或递减。根据对信号量值的运算结果,可以判断它是否具有对公共资源的访问权限。当信号量值大于0时,可以访问,否则会被阻塞。但是即使在多线程程序中,也可以保证对它的访问是原子的。

信号量类型:

二进制信号量,它只有两个值,0 和 1。对于一次只能由一个执行线程运行的关键代码,使用二进制信号量。计数信号量。它可以有更大的取值范围,适用于关键代码允许有限数量的线程执行,需要使用计数信号量。信号量 API

信号量 API

协程服务模型

什么是协程

什么是协程?协程协程是比线程更轻量级的微线程。类似地,一个进程可以有多个线程,一个线程也可以有多个协程,所以协程也被称为微线程和纤程。

协程图

协程可以大致理解为子程序调用,每个子程序都可以在单独的协程中执行。

协程子程序模型

协程服务模型

为了说明什么是协程模型,我们以多线程下的生产者-消费者模型为例。

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

函数伪代码

多线程执行过程如下:

生产者线程首先调用函数 Do_some_IO 执行耗时的 IO 操作,例如从网络套接字读取数据。消费者线程阻塞并等待,直到生产者线程执行 Do_some_IO 完成数据读取。生产者线程阻塞并等待,直到消费者线程执行 Do_some_process 完成数据处理。消费者线程执行Do_some_process完成数据处理后,通知生产者线程继续Do_some_IO

线程执行时间线可以看出,为了保证每个线程并行工作,多线程模型需要在线程之间做大量的同步和通知工作,线程频繁在阻塞和唤醒之间切换。我们知道Linux下的线程是轻量级线程LWP,每次线程切换都涉及到用户态和内核态的切换,还是很耗性能的。

在协程模型中如何处理相同的场景?还是用前面的例子来说明协程模型的执行流程。

Do_some_IO()// IO处理协程 Do_some_process() // 计算处理协程

分配生产者协程执行Do_some_IO进行IO处理操作,分配消费者协程进行Do_some_process计算处理操作。当生产者协程工作时,消费者协程一直在等待。生产者协程完成IO处理后,将处理结果返回给消费者,将程序执行权限交给消费者协程向下执行。

协程执行时间线

协程优势

由于协程是在线程中实现的,所以始终是一个线程来操作共享资源,所以不存在多线程抢占资源和资源同步问题。生产者协程和消费者协程相互协作完成工作,而不是相互抢占,协程创建和切换的开销远小于线程。

硬件提高性能

上面所说的多线程、多进程、协程只是对服务处理能力的软件级增强。真正的硬核是从硬件层面提高处理能力,增加CPU的物理核心数。当然,硬件是有成本的,所以只有在软件层面完全耗尽了性能的时候,才会考虑增加硬件。

不过,老板有钱买最好最贵的服务器。这就是RMB玩家和穷玩家的区别。软件工程师留下了贫穷的泪水。

增加机器核心数

在 CPU 世界中有一条摩尔定律:大约 18 个月可以将芯片的性能提高一倍。现在这个规律越来越难以打破,提高CPU晶体管密度的工作频率也越来越难。相反,通过增加 CPU 内核的数量来提高处理器性能。

目前,商用服务器架构基本上都是多核处理器。多核处理器可以真正并行运行程序,大大提高处理效率。如何查看CPU核数?

对于Windows操作系统,打开任务管理器,通过界面的“内核”和“逻辑处理器”可以看到。

Windows 视图核心

查看CPU核心数

对于Linux操作系统,通过以下2种方式查看CPU内核相关信息。

1.通过cpuinfo文件查看

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

processor进程共享内存读写锁,表示cpu处理器的数量 cpu cores,表示每个处理器的核心数量 cpuinfo输出示例:

cpu信息

2.通过编程界面查看

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

查看核心 API

CPU 亲和性

CPU亲和性是将一个进程或线程绑定到一个特定的CPU或一组CPU上进程共享内存读写锁,使得该进程或线程只能被调度在绑定的CPU或一组CPU上运行。

为什么要设置 CPU 亲和性来绑定 CPU?理论上,进程最后一次运行后的上下文信息会保留在 CPU 缓存中。如果该进程下次仍被调度到同一个 CPU 上,则可以避免缓存未命中对 CPU 处理性能的影响,从而使该进程运行起来。更高效。

如果某些进程或线程是CPU密集型的,不想被频繁调度,或者你有其他特殊需求,不希望进程或线程被调度频繁在不同CPU之间切换,可以对线程进行绑定到特定的CPU,可以优化特定场景下的程序性能。

绑定过程

在多进程模型中,一个进程被绑定到一个特定的核上,下面是绑定一个进程的系统API

绑定线程

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

设置线程亲和性

总结

本文从程序任务的类型入手,将任务分为两类:CPU密集型和IO密集型。接下来分别介绍了基于这两类任务提高服务性能的方法,分为软件级方法和硬件级方法。

软件层面主要描述多进程、多线程和协程模型的使用。当然,现有的技术还有IO复用、异步IO、池化技术等解决方案。说到多线程和多进程,就利用情况介绍进程间通信和线程间同步互斥技术。

第二部分从硬件层面讲解如何提升服务性能:增加机器核数,教你如何查看CPU核数。最后,还可以通过软硬件结合的方式,将硬件内核绑定到指定的进程或线程上执行,从而最大限度地利用CPU性能。

希望通过本文的学习,读者对高性能服务模型有一个初步的了解,并能举例说明服务优化的方法和优缺点,这就是本文的价值所在。

感谢您的阅读。文章的目的是分享对知识的理解。技术文章我会反复验证,最大程度保证准确性。如果文章有明显错误,请指出,我们一起在讨论中学习。

如果你觉得文章写得好,对你有帮助,不要白投柠檬,动动手指“看”或“分享”是对我不断创作的最大支持。

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论