探索分析并发控制的关键作用 — 确保系统稳定与高效的技术导论(修正版)

222 阅读23分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


为什么选择并发技术

为了提升系统的整体效能与用户体验,致力于实现响应延迟的最小化策略,同时力求最大化吞吐量,确保系统在高负载下仍能保持高效运作。这一目标的实现,离不开对自治对象行为的精准建模与模拟,通过先进的算法与技术,使这些对象能够自主决策、灵活应对,从而优化系统流程。

什么是并发程序

并发程序的设计与实施涉及到一个微妙的平衡:它们既可以确实地实现多任务的同时处理(在资源充足且调度得当的情况下),也可能因为资源竞争、同步问题或设计不当而未能充分展现预期的并行优势,甚至在某些情况下退化为看似并行实则串行的执行模式。

1. 跨多个cpu操作

在涉及跨越多重计算环境的操作中,包括但不限于多核心CPU的利用、对称多处理器(SMP)架构的部署、集群系统的构建,乃至特殊用途的体系结构应用,核心挑战之一在于高效地管理与共享关键资源。

2. 并行的技术机制

在并行编程的广阔天地里,开发者致力于探索如何将复杂的软件任务拆解成多个可并行处理的子任务,并分配给不同的CPU进行处理。这种分布式计算方式能够充分利用多核处理器的计算能力,有效缩短任务执行时间,提高资源利用率,从而显著提升系统的整体性能表现。

并发架构可用性

在持续优化系统性能的过程中,专注于两大核心目标:显著减少响应延迟最大化提升吞吐量

响应延迟

优化系统架构与流程设计,确保每个请求都能以最短的时间得到处理。通过深入分析系统瓶颈,实施了高效的资源调度与负载均衡机制,有效减少了请求等待时间,主要考虑额的方案实现为横向拆分和纵向拆分两个大的方向和目标:

_显著减少响应延迟.png

  • 纵向拆分-微服务架构:将大型系统拆分为多个小型、独立的服务,每个服务运行在其独立的进程中,通过轻量级的通信机制(如HTTP RESTful API)进行通信,如下图所示:

项目微服务架构图.png

当原有的耗时操作被集中处理于单次请求或单一服务流程中时,往往会导致响应周期显著延长,进而延迟了对客户端请求的反馈控制。如下图所示,系统的一次调用需要经历多个领域接口的聚合参会得出结果,而数据可能还集中于一个服务的场景下,服务的负荷也急剧上升。

未命名表单.png

注意,这种延迟不仅加剧了用户的等待体验,还可能触发互联网服务中普遍遵循的“两秒法则”问题,即若页面加载或响应超过两秒,用户满意度将大幅下降。

如果我们通过对服务能力实施更深入的拆分与职责的精细化界定,不仅能够有效分散服务节点所承载的复杂性与工作负担,还促使各节点支撑压力得以逐步缓解并趋于优化。可以看到改良之后的架构分析说明:

未命名表单.png

从上图可以看出来,我们将服务的领域以及职责进行了边界上下文的界定和拆分,从而基于DDD的领域驱动能力进行深度分析和细化,从而可以得到将页面或者服务能力进行独立化处理,从而降低了由于组合和等待所引发的延迟问题。

数据公式对比

未拆分服务的耗时计算逻辑为(服务数量)N* 单个服务(平均耗时) VS 拆分服务的耗时计算逻辑为 1 * 单个服务耗时(最长耗时),因为我们采用并行处理逻辑+分布式拆分服务,出现了优化的空间。

  • 横向拆分-分层架构设计:将系统划分为不同的逻辑层次,每一层只关注自己的任务,减少层间的耦合,提高系统的清晰度和可维护性,同时也有助于识别和隔离性能瓶颈。

Spring Cloud 微服务总体架构图.png

上面介绍了基于不同的领域结合分布式并发和并行技术实现了一部分的接口吞吐和性能的优化逻辑实现,接下来针对于内部实现逻辑的复杂度以逻辑进行进一步拆分,大家可以跟随我们的脚步,基于纵向拆分之后的架构进行进一步的分析和说明:

未命名表单.png

从上面可以看出又进行了进一步的拆分,也标志着我们的服务变得更加微观化,因为不同的层次服务可以分不到不同的主机节点上,因此,每一个服务的职责会被进一步划分,因此可以更加充分的利用对应的每个机器节点,进行深度的优化和分散压力,从而提升效率和能力。

数据公式对比

拆分服务的耗时计算逻辑为 1 * 单个服务耗时(最长耗时),因为我们采用并行处理逻辑+分布式拆分服务,出现了优化的空间 VS 纵横拆分服务的耗时计算逻辑为(服务数量)N* 单个服务(平均耗时)* M(机器节点数)。

提升吞吐量

利用现代计算资源,特别是多核处理器与分布式系统的优势。通过并行处理与异步编程技术,实现了多个任务的同时执行,显著提高了系统的处理能力。

未命名表单.png

通过采用多核心处理技术,我们能够显著提升系统效率,实现多任务并发执行的新高度。这一技术精髓在于,它能够驱动多个线程在并行轨道上疾驰,每个线程独立承担并高效处理其专属任务。进一步地,通过精心设计的任务分配策略,我们能够将不同的数据操作智能地映射到不同的CPU核心上,确保每个核心都能专注于其最擅长的处理领域,从而最大化资源利用率。

网状拓扑结构图.png

这种并行处理模式的实现,不仅极大地缩短了任务完成的整体时间,还通过减少等待时间和优化资源分配,提升了系统的整体响应速度和吞吐量。

数据公式对比

纵横拆分服务的耗时计算逻辑为(服务数量)N* 单个服务(平均耗时)* M(机器节点数) VS 纵横拆分+并行技术为:(服务数量)N* 单个服务(平均耗时)* M(机器节点数)* X(CPU物理内核核心数) 。

但是基于并行化处理逻辑,刚刚也是我们简单的分析和描述说明一下,接下来我们要深入分析并行技术的两大特色能力:

  • 多处理器系统:为提供了强大的并行处理能力,精心设计任务分配策略,确保不同处理器能够协同工作,共同承担系统的计算负载,通过精细的负载均衡与资源调度,实现了计算资源的最大化利用,有效缩短了任务执行时间。

  • 重叠的I/O技术:系统在执行计算任务的同时,也能够进行输入/输出操作,从而避免了传统模式下计算与I/O操作的串行等待。通过优化I/O请求的调度与管理,确保了计算与I/O操作的并行进行,显著提高了系统的整体吞吐量与响应速度。

多处理器系统

上面简单的说明了,多处理器系统所带来的性能提升,接下来,我们来详细分析和介绍一下什么是多处理器系统,以及其底层相关的实现逻辑,主要将多处理器的技术需求分为以下几点:

多处理器系统.png

多处理器架构

在深入探讨多处理器系统的核心技术时,其基石无疑是多处理器架构的设计。当前,这一领域主要呈现出两大标志性架构类型:对称多处理(SMP)与非对称多处理(ASMP),它们各自以其独特的方式引领着多处理器系统的发展潮流。

  • 对称多处理(SMP)架构:多处理器设计的一个典范,其核心在于所有处理器单元在系统中的地位与权限是完全平等的。

未命名表单.png

这种架构下,处理器能够无差别地访问共享资源,如内存池和外设接口,从而实现了任务执行的极致并行化,处理器之间通过高速总线或交叉开关进行通信,确保数据快速、准确地传输。

未命名表单.png

通过高效的内部通信机制,如高速总线或交叉开关网络,SMP架构确保了数据在处理器间的快速流通与同步,极大地提升了系统的整体性能与响应速度。

缓存一致性协议(如MESI协议)用于维护多个处理器缓存之间的一致性,确保数据在多个处理器间的同步

  • 非对称多处理(ASMP)架构:展现了一种更为灵活且层次化的处理器协作模式,多处理器系统需要高效地管理共享资源,如内存、外设等。通过锁机制、信号量等同步机制,确保多个处理器在访问共享资源时不会发生冲突。

未命名表单.png

在此架构中,处理器被赋予了不同的角色与职责,其中主处理器(或称为管理处理器)负责系统的整体调度与管理,而其余处理器则作为从属单元,专注于执行具体的计算任务。

注意,操作系统负责将任务分配给不同的处理器,以实现负载均衡,任务调度算法(如轮转调度、优先级调度等)根据任务的优先级和系统的负载情况,合理地将任务分配给各个处理器


多处理器系统引发的问题

上面主要介绍了多处理器系统,作为现代计算技术的核心,虽然极大地提升了计算能力和效率,但也随之带来了一系列复杂而深刻的问题,目前主要有数据安全问题线程活跃性问题这两大问题。

多处理器系统引发的问题.png

数据安全问题

多线程环境下的数据安全,核心在于确保共享数据的正确性和稳定性,避免数据不一致或异常状况的发生。数据安全问题的根源通常包括数据竞争/竞态条件

数据竞争/竞态条件

数据竞争,也常被称为“竞态条件”在数据处理领域的一个具体表现,是指当多个进程或线程在没有适当同步机制的情况下,同时访问并尝试修改共享数据时发生的冲突。这种冲突可能导致数据的不一致性和错误的结果。

数据竞争_竞态条件.png

场景一:多线程访问共享资源

在多线程编程中,竞态条件是一个常见问题。当多个线程尝试同时访问并修改共享变量(如计数器、标志位等)时,如果没有适当的同步(如互斥锁、信号量等),就可能导致竞态条件。

未命名表单.png

例如,计数器中的add方法,在没有同步保护的情况下,多个线程可能同时读取相同的初始值,然后各自加上不同的值并写回,导致最终结果不正确,如上图中的线程2和线程3所读取的a变量得到值就是不一致的。

场景二:网络中的资源争用

在网络环境中,竞态条件也可能发生在多个用户或设备同时尝试访问同一资源时,建立socket端口的时候,数据存储冲突的问题场景:

未命名表单.png

例如,在两个用户几乎同时尝试通过同一个网络端口发送数据时,如果没有适当的冲突解决机制(如TCP的三次握手),就可能发生数据包丢失或乱序等问题。

1723284743711.png

场景三:数据库事务处理问题

在数据库系统中,竞态条件也是一个重要问题。

未命名表单.png

当多个事务同时尝试修改同一数据时,如果没有适当的事务隔离级别和锁机制,就可能发生脏读、不可重复读和幻读等问题。

线程活跃性

线程存活与活跃性问题关注的是线程能否持续执行其预定任务,以及是否存在因资源竞争或程序缺陷导致的线程停滞。这些问题包括死锁、活锁和线程饥饿。

死锁

多线程中一种常见的活性故障,发生在多个线程相互等待对方释放资源而无法继续执行的情况。死锁往往发生在进程相互等待对方释放资源的情况下。

未命名表单.png

解决死锁的策略包括避免循环等待条件、确保资源的可抢占性以及使用超时机制等。

避免循环等待

策略之一是预防循环等待条件的形成,这要求在设计系统资源分配机制时,确保资源的请求顺序在多个进程或线程之间保持一致性或采用动态分配策略,从而从根本上打破死锁产生的循环依赖链。

未命名表单.png

在处理不同的代码块之间交互时,我们的核心目标是减少并消除锁操作的交叉现象,以此作为预防死锁的重要策略。参考所提供的图示分析,调整了代码结构与锁管理机制的关联性,确保代码的执行逻辑不仅避免了不必要的交错执行,还尽量趋向于遵循一致的加锁顺序。

强化了对锁顺序的一致性管理,即确保在多个代码块请求资源时,按照固定的顺序来获取锁,这种策略显著降低了死锁的发生概率,因为它破坏了死锁形成的关键条件——循环等待。

确保资源可抢占

确保资源可抢占意味着系统中的某些资源在被分配给某个进程后,系统仍然有能力(在必要时)将这些资源从该进程中收回,并重新分配给其他需要的进程,例如我们可以针对于不同的代码块之间的交叉场景加入【优先抢占能力】。

优先级抢占

假设系统为执行程序代码块设置了优先级,且代码块1的优先级高于代码块2。当系统检测到死锁时(尝试重试抢占锁N次),它可以强制将资源代码块2中剥夺锁,并将其分配给代码块1。

未命名表单.png

通过允许资源抢占,系统可以干预并重新分配资源,从而解除死锁状态。

超时控制实现

通过定时抢占解决死锁问题,线程P1和P2的请求设置了超时时间。假设P1的请求超时,可以主动/被动的方式将P1持有的锁释放或者中断,并将其置于等待状态。

未命名表单.png

之后,尝试将锁主动/被动得到分配给P2。如果P2成功获得锁并完成其任务后释放锁,则P1可以重新尝试获取锁。

活锁问题

活锁(Livelock)是并发系统中一种特殊的现象,它发生在多个线程或进程在执行过程中,尽管它们都没有被阻塞(即都在积极地运行),但由于它们之间的相互作用和“礼貌”的谦让行为,导致这些线程或进程无法继续向前推进以完成它们的任务。

解决方案

解决活锁的一种有效方法是引入随机等待时间,以减少线程间冲突的概率,总体大致分为以下五种方式:

  • 打破循环依赖:通过确保资源的获取顺序一致(如使用锁的顺序一致性)来避免,这种方式和解决死锁的逻辑相同,这里就不进行赘述。
  • 使用超时机制:尝试获取资源时设置超时时间,如果超时则放弃当前尝试,可能采取其他策略(如重试、回退等),这种方式和解决死锁的逻辑相同,这里就不进行赘述。
  • 中央协调器:中央协调器或仲裁者来管理资源的分配,确保不会发生循环等待,这种方式和解决死锁的逻辑相同,这里就不进行赘述。

接下来,我们主要方向分析一下对应的使用随机性的方案:

引入随机性

在尝试获取资源或执行操作时引入随机性,以减少或避免循环等待的模式。随机性意味着线程或进程在决定下一步行动时不再总是遵循相同的规则或顺序,而是有一定的概率选择多种可能的行动之一。

随机性的实现方式.png

  • 随机睡眠:在尝试获取资源或执行操作之前,线程或进程可以随机地等待一段时间(称为“随机睡眠”)。这段时间可以是几毫秒到几秒不等,具体取决于系统的具体需求和资源情况。

    int sleepMill = RandomUtil.randomInt(100,200);
    Thread.sleep(sleepMill);
    // 进行加锁操作,此种方式,通过暂停或者休眠一个随机中,从而避免活锁。
    
  • 随机选择资源:如果线程或进程需要访问多个资源,并且这些资源的访问顺序可能导致活锁,那么它们可以随机地选择先访问哪个资源。

未命名表单.png

  • 随机决策点:在程序中设置随机决策点,使线程或进程在到达这些点时随机地选择不同的执行路径。这可以通过生成随机数并与预设的阈值进行比较来实现。
    int randomValue = RandomUtil.randomInt(100,200);
    if(randomValue+条件1){
     // 场景1处理逻辑,加锁
    }
    else if(randomValue+条件2){
     // 场景2处理逻辑,加锁
    }
    // 进行加锁操作,此种方式,通过暂停或者休眠一个随机中,从而避免活锁。
    
线程饥饿

线程因无法访问所需资源而无法继续执行,这可能是由于线程优先级设置不当或持有锁的线程执行时间过长导致的。解决线程饥饿的方法包括使用公平锁、优化锁持有时间以及合理规划线程的执行顺序。

未命名表单.png

经过前述的深入剖析,我们可以明确,在存在非公平调度机制的情况下,活跃性交叉的线程现象尤为显著。这主要是由于新创建的线程,在未经任何优先权考量的情况下,倾向于频繁地抢在优先级较低的老线程之前执行,从而扰乱了资源的自然分配顺序。

使用公平锁

为了有效应对这一问题,并确保资源获取过程的有序性与公平性,我们转而诉诸于支持公平锁的环境。

未命名表单.png

在这样的环境中,通过实施公平锁机制,能够确保线程在竞争共享资源时,严格按照某种预定的顺序(如创建顺序或优先级顺序)进行,从而极大地减少了因无序竞争而导致的活跃性交叉问题,保障了系统资源分配的稳定性和可预测性。


高并发架构的场景

并发性作为软件设计中一个核心且富有深度的概念性属性,其本质在于程序执行过程中能够同时处理多个任务或操作,而这些任务或操作在逻辑上可能并行进行,但在物理层面上可能因资源限制(如处理器时间、内存访问等)而采用交替执行的方式。

在这里插入图片描述


(可选-额外补充)扩展模型技术

在深入探讨并发技术的广阔领域时,我们即将聚焦于一个核心议题——对象模型的细致分析与详尽阐述。并发技术,作为现代软件开发中不可或缺的一环,其高效运作与对象模型的设计和实现紧密相连。因此,对对象模型进行深入的剖析,不仅有助于我们更好地理解并发技术的本质,还能为优化系统性能、提升软件质量提供有力支持。

在这里插入图片描述

  • 资源对象的共享:通过高级编程技术和框架,多个进程或线程可以安全地访问和操作同一资源对象,如数据库连接、缓存实例等,从而避免了资源的重复创建与消耗,实现了资源的最大化利用。

  • 内存空间的共享:在现代操作系统中,内存共享机制允许不同的程序段或进程间直接访问同一块内存区域。这种机制极大地提高了数据交换的速度和效率,同时也需要严格的访问控制和同步机制来确保数据的一致性和安全性。

  • 文件描述符的共享:文件描述符是操作系统中用于表示打开文件的唯一标识符。通过文件描述符的共享,多个进程可以共同访问和操作同一个文件,这在数据共享、日志记录等场景中尤为重要。

  • 套接字的共享:在网络编程中,套接字(Socket)是通信的基石。虽然传统上套接字不直接支持共享,但可以通过高级网络协议、中间件或代理服务来实现套接字的间接共享,从而实现多客户端与同一服务器的通信,或者多个服务器之间的数据交换。

对象模型

对象模型,作为一种抽象化的表达工具,其核心在于精准而全面地刻画出一系列对象(无论其是否具备正式定义)所共有的核心特征与属性。这一过程不仅仅是简单的信息罗列,更是对对象间深层次联系与规律的提炼与总结。

四大基础操作

在这里插入图片描述

这些操作所遵循的规则各具特色,形成了鲜明的差异,并可以主要划分为两大截然不同的类别。每一类别均承载着其独特的逻辑原理与应用场景。

主动与被动并行模型

主动与被动并行模型,作为并发编程领域内的两大核心范畴,各自蕴含着促使独特并发面向对象(OO)设计模式诞生的关键特性。这两种模型不仅体现了并行计算的不同实现策略,还深刻影响了面向对象设计中对并发性的处理方式和系统架构的塑造。

主动并行模型

主动并行模型侧重于对象的自主性与独立性,鼓励对象之间通过事件、消息或信号等方式进行异步通信与协作,从而实现任务的并行执行。

每个实体(在此语境中可视为对象的高级抽象)均配备有一个专属的控制流单元,类似于进程的角色,但专注于单一任务处理的深度而非广度。这种设计意味着,在任何给定时刻,该实体专注于执行单一操作,体现了串行处理的原则。 在这里插入图片描述

信息传递在这一架构中是严格单向的,确保了系统的清晰性和可预测性,同时允许其他通信协议或机制以分层方式叠加,以增强功能或优化性能。

至于交互细节与行为模式,系统提供了丰富的选项与扩展点,包括但不限于异步与同步消息交换机制,后者确保了消息传递的即时性与顺序性,而前者则通过非阻塞方式提升了系统响应能力和吞吐量。

这一模型促进了响应式编程、事件驱动架构等并发设计模式的兴起,使得系统能够更灵活地应对并发请求,提高资源利用率和响应速度。

被动并行模型

被动并行模型则更加依赖于外部调度器或协调者的控制,对象本身可能不具备直接的并行执行能力,而是需要在外部指令的驱动下参与并发过程。这种模型下,并发设计往往围绕任务分配、资源管理和同步机制等核心问题展开,促进了如线程池、任务队列等并发设计模式的广泛应用。

在这里插入图片描述在顺序程序中,只有单个程序对象是主动的被动对象作为程序的数据,在单线程Java中,程序是JVM(解释器),顺序地模拟包含程序的对象,基于过程调用的所有内部通信。

并发对象模型

主动与被动对象混合体与纯粹被动对象系统之间的线程数量差异时,这种混合架构往往展现出对线程资源更为优化的利用。具体而言,主动对象,顾名思义,是那些能够主动执行操作、响应内部状态变化或根据预设逻辑自主行动的对象。相比之下,被动对象则更多地依赖于外部刺激(如消息或函数调用)来触发其行为。 在这里插入图片描述

共享内存多处理

在讨论同一(虚拟)机器内部对象的通信机制时,过程消息传递作为一种常见的交互方式,其效率与资源利用的特点值得深入探讨。通常,人们可能会认为增加CPU的线程数量能够直接提升系统的并行处理能力和响应速度,但在实际应用中,情况往往更为复杂。

远程消息传递

在探讨对象访问模式与数据交换机制时,我们不难发现,特定条件下的数据封送(即序列化过程)是确保数据完整性和可访问性的关键环节。具体而言,当对象访问仅限于远程引用或数据复制的方式时,数据封送成为了不可或缺的一步。