进程、线程、协程
-
进程:独立的程序实例,资源开销较大,适合隔离性要求高的任务。
-
独立性:进程具有独立的内存空间和资源,互不干扰。
-
资源开销大:由于每个进程都需要分配独立的内存和资源,创建和切换进程的开销相对较大。
-
进程间通信复杂:进程之间的通信通常需要通过操作系统提供的机制,如管道、消息队列或共享内存
-
-
线程:进程中的执行单元,轻量级,适合需要共享资源的并发任务。
-
共享内存空间:同一进程中的所有线程共享相同的内存地址空间,因此线程之间的通信比进程之间更容易。
-
轻量级:线程创建和切换的开销比进程小,因为线程间共享资源,不需要像进程那样进行大量的上下文切换。
-
并发执行:在多核处理器上,不同线程可以真正实现并行运行。
-
-
协程:比线程更轻量的并发执行单元,适合 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 aiohttp
async 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 asyncio
async def task(name, delay):
await asyncio.sleep(delay)
print(f"Task {name} completed.")
return name
async 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技术,推迟加载非关键资源,以减少页面加载时的并发请求数。