浅谈进程,线程,协程以及服务端高并发的处理

2024-08-23 02:04

本文主要是介绍浅谈进程,线程,协程以及服务端高并发的处理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

进程、线程、协程

  • 进程:独立的程序实例,资源开销较大,适合隔离性要求高的任务。

    • 独立性:进程具有独立的内存空间和资源,互不干扰。

    • 资源开销大:由于每个进程都需要分配独立的内存和资源,创建和切换进程的开销相对较大。

    • 进程间通信复杂:进程之间的通信通常需要通过操作系统提供的机制,如管道、消息队列或共享内存

  • 线程:进程中的执行单元,轻量级,适合需要共享资源的并发任务。

    • 共享内存空间:同一进程中的所有线程共享相同的内存地址空间,因此线程之间的通信比进程之间更容易。

    • 轻量级:线程创建和切换的开销比进程小,因为线程间共享资源,不需要像进程那样进行大量的上下文切换。

    • 并发执行:在多核处理器上,不同线程可以真正实现并行运行。

  • 协程:比线程更轻量的并发执行单元,适合 I/O 密集型任务和对性能要求较高的场景。

    • 轻量级:协程比线程更轻量,通常不需要像线程那样的上下文切换开销,因为它们在用户态完成调度。

    • 非抢占式多任务:协程的切换是显式的,只有当协程主动让出执行权时,才会切换到其他协程。这使得协程之间的并发执行更加可控。

    • 无需多线程环境:协程在单线程环境中就能实现高效的并发,因此非常适合 I/O 密集型操作,如网络请求或文件读取。

进程出现的目的,是为了更好的利用CPU资源。

例如:

假设有两个任务A和B,当A遇到IO操作,CPU默默地等待任务A读取完操作再去执行任务B,这样无疑是对CPU资源的极大的浪费。若在任务A读取数据时,让任务B执行,当任务A读取完数据后,再切换到任务A执行,这样就可以更好地利用CPU资源。这里的切换涉及到状态的保存,状态的恢复,需要有一个东西去记录任务A和任务B分别需要什么资源,怎样去识别任务A和任务B,这时进程就出现了。

因此,通过进程来分配系统资源,标识任务。

如何分配CPU去执行进程称之为调度,进程状态的记录,恢复,上下文切换(简称切换)。

其次,若上面提及的任务A是一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。

若只有一个进程,会造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西-------文本内容,不停地切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。这时线程出现了。

线程共享进程的大部分资源,并参与CPU的调度。

当操作系统切换线程时,需要保存当前线程的状态(如寄存器、程序计数器、堆栈指针等),然后加载下一个线程的状态,这个过程被称为上下文切换。线程上下文切换通常涉及系统调用和内核参与,因为线程通常由操作系统内核来管理。

假设当涉及到大规模的并发请求连接时,例如有一万个人同时连接我的服务器,但系统资源有限,如果以线程作为处理单元,调内部系统资源的话大部分线程都处于等待状态

传统的请求是线程池+一个请求创建一个线程 (java应用服务器tomcat,Jetty) 由于是这种方式有很大的并发限制所以有了如下的优化:

  • NIO 提供了非阻塞的 I/O 操作,这使得线程不需要等待 I/O 操作完成,而是可以在 I/O 准备好时被通知。提高了线程利用率。由于一个线程可以管理多个通道,NIO 在高并发场景下能显著减少线程数量,从而提高性能。

  • 线程池,通常使用线程池管理线程。线程池限制了同时运行的线程数,并通过复用线程来减少资源消耗。

局限性:

  • 资源开销:每个线程都占用一定的系统资源,包括内存、CPU 时间、操作系统调度等。当有大量线程(如一万个线程)同时运行时,系统的内存和 CPU 资源很快就会被耗尽。

  • 上下文切换:当多个线程在 CPU 上并发运行时,操作系统内核需要频繁地进行上下文切换,保存和恢复每个线程的状态。这种上下文切换是有开销的,尤其是在大量线程之间频繁切换时,会导致系统性能下降,很多时间可能都浪费在切换上下文上,而不是实际处理任务。

协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。

使用协程就可以实现线程自己调度,不陷入内核级别的上下文切换。协程可以在 I/O 操作时主动让出 CPU,线程不会被阻塞,其他协程可以继续执行。这样,单个线程可以管理多个协程,从而极大地提高了并发处理能力,充分利用 CPU 资源。

协程切换是在用户态完成的,不涉及系统调用。协程之间的切换通常只需要保存和恢复少量的状态信息,切换开销极小,因此在高并发场景下,协程的性能优势非常明显。

为什么协程不需要经过内核级别的上下文切换,我是这样认为的:

内核态 vs. 用户态:操作系统运行在两种模式下:内核态和用户态。内核态拥有对硬件和系统资源的完全控制,用户态则是普通应用程序运行的状态。线程切换通常需要切换到内核态进行调度,协程的调度和切换不需要通过系统调用来进行,完全在用户态操作。这意味着协程切换不会触发内核级别的上下文切换,不涉及操作系统的调度器,也不需要保存和恢复操作系统管理的线程栈和寄存器状态等。

进程和线程都是操作系统自带的,协程是有些程序原生支持的,例如go,lua, 有些是后期版本才有的,比如python2.5 以后

Python:Python 的 asyncio 库是一个常用的协程框架,可以轻松创建和管理协程。使用 asyncio,一个线程可以处理很多的并发任务,例如网络 I/O、数据库查询等。

JavaScript:JavaScript 的异步操作(如 Promise 和 async/await)允许在单线程环境中实现协程式的并发处理。Node.js 就利用了这种模型,允许在单线程中处理大量并发 I/O 操作。

Python 协程的举例:

异步IO操作:

import asyncio
import aiohttpasync def download_file(session, url):async with session.get(url) as response:content = await response.read()filename = url.split('/')[-1]with open(filename, 'wb') as f:f.write(content)print(f"Downloaded {filename}")async def main():urls = ['http://example.com/file1','http://example.com/file2','http://example.com/file3']async with aiohttp.ClientSession() as session:tasks = [download_file(session, url) for url in urls]await asyncio.gather(*tasks)asyncio.run(main())

并发任务的协调与管理

import asyncioasync def task(name, delay):await asyncio.sleep(delay)print(f"Task {name} completed.")return nameasync def main():# A,B并发执行 两个协程task_a = asyncio.create_task(task("A", 3))task_b = asyncio.create_task(task("B", 2))await asyncio.gather(task_a, task_b)# Task C 在A,B后执行 C 可能依赖AB 的结果await task("C", 1)asyncio.run(main())

await 关键字用于暂停协程的执行,直到 await 后面的协程对象完成。它使得协程可以异步地等待其他协程的结果,而不会阻塞事件循环。

服务端高并发处理

对于服务端提高并发常见的处理方案

常见的系统架构图

1. 负载均衡

  • 水平扩展(Scale-out):通过增加服务器数量,使用负载均衡器(如Nginx)将请求分发到多台服务器上,从而分散压力。负载均衡器可以基于不同的策略(如轮询、最小连接数、IP哈希等)将流量分配给各个后端服务器。

2. 缓存技术

  • 数据缓存:使用缓存机制(如Redis、Memcached)缓存热点数据,减少数据库的查询次数。常见的缓存策略有本地缓存、分布式缓存等。

  • 页面缓存:对于动态生成的页面,尤其是变化不频繁的页面,可以将其缓存一段时间,从而减少服务器的计算负载。

3. 数据库优化

  • 读写分离:通过主从复制,将读操作分配到从数据库,写操作由主数据库处理,从而减轻主数据库的负载。

  • 分库分表:当单一数据库实例无法承载大量数据时,可以将数据按某种规则拆分到多个数据库(分库)或多个表(分表)中。

  • 索引优化:合理使用数据库索引,加速查询速度,同时避免过多的索引导致的写操作性能下降。

  • 使用NoSQL数据库:在某些场景下(如处理海量数据、高频写入等),可以使用NoSQL数据库(如MongoDB、Cassandra、HBase等)替代传统的关系型数据库。

4. 异步处理与消息队列

  • 异步处理:将非实时任务(如日志记录、通知发送、复杂计算)通过异步方式处理,减少主线程的负载。可以使用事件驱动或基于回调的异步编程模型(如Node.js、Python的异步IO)。

  • 消息队列:使用消息队列(如RabbitMQ、Kafka、ActiveMQ)解耦各个系统组件,将需要排队的任务放入队列中,异步处理,从而避免系统过载。

5. 服务拆分与微服务架构

  • 服务拆分:将单一的大型应用程序拆分为多个独立的服务,减少耦合,增强系统的可扩展性。每个服务可以独立扩展并优化,从而提高系统整体的并发处理能力。

  • 微服务架构:采用微服务架构,将不同的业务逻辑分离成独立的微服务,通过RESTful API或消息队列通信,增强系统的灵活性和伸缩性。

6. 限流与降级

  • 限流:对接口或服务设置访问频率限制,防止因瞬时流量过大导致系统崩溃。可以采用令牌桶算法、漏桶算法等限流策略。

  • 熔断与降级:当系统部分组件过载或不可用时,及时熔断(停止调用)该组件,并执行降级策略(如返回默认值、提供备用服务),以保证核心功能的正常运行。

7. 操作系统与Web服务器调优

  • 操作系统调优:调整操作系统的网络参数(如文件描述符上限、TCP连接数等),优化内存管理,减少上下文切换,提升并发处理能力。

  • Web服务器调优:调整Web服务器(如Nginx、Apache、Tomcat等)的并发连接数、线程池大小、缓存配置等参数,以适应高并发环境。

8. 前端优化

  • 减少请求数 减少服务器压力。

  • 延迟加载:使用Lazy Load技术,推迟加载非关键资源,以减少页面加载时的并发请求数。

这篇关于浅谈进程,线程,协程以及服务端高并发的处理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1098036

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

浅谈主机加固,六种有效的主机加固方法

在数字化时代,数据的价值不言而喻,但随之而来的安全威胁也日益严峻。从勒索病毒到内部泄露,企业的数据安全面临着前所未有的挑战。为了应对这些挑战,一种全新的主机加固解决方案应运而生。 MCK主机加固解决方案,采用先进的安全容器中间件技术,构建起一套内核级的纵深立体防护体系。这一体系突破了传统安全防护的局限,即使在管理员权限被恶意利用的情况下,也能确保服务器的安全稳定运行。 普适主机加固措施:

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

Thymeleaf:生成静态文件及异常处理java.lang.NoClassDefFoundError: ognl/PropertyAccessor

我们需要引入包: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>sp

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

Java Websocket实例【服务端与客户端实现全双工通讯】

Java Websocket实例【服务端与客户端实现全双工通讯】 现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发 出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏 览器需要不断的向服务器发出请求,然而HTTP

jenkins 插件执行shell命令时,提示“Command not found”处理方法

首先提示找不到“Command not found,可能我们第一反应是查看目标机器是否已支持该命令,不过如果相信能找到这里来的朋友估计遇到的跟我一样,其实目标机器是没有问题的通过一些远程工具执行shell命令是可以执行。奇怪的就是通过jenkinsSSH插件无法执行,经一番折腾各种搜索发现是jenkins没有加载/etc/profile导致。 【解决办法】: 需要在jenkins调用shell脚

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api