并发编程核心概念详解:进程、线程与协程的本质与差异
目录
一、什么是进程?
1.1 定义
1.2 进程的特点
1.3 进程的状态
二、什么是线程?
2.1 定义
2.2 线程的优缺点
2.3 进程和线程之间的区别和联系
三、什么是协程?
3.1 定义
3.2 与多线程相比,协程的优势
3.3 与虚拟线程的关键区别
3.4 代码示例(python)
进程(Process):是资源分配的最小单位,一个进程可以有多个线程,多个线程共享进程的堆和方法区资源,不共享栈、程序计数器。
线程(Thread):是任务调度和执行的最小单位,线程并行执行存在资源竞争和上下文切换的问题。
协程(Coroutine):是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
一、什么是进程?
1.1 定义
打开微信会产生一个进程,打开QQ会产生一个进程,电脑上运行的程序都是进程。
直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
进程之间无法直接访问彼此的资源。
1.2 进程的特点
- 独立性:每个进程都有自己的地址空间,彼此独立,互不影响。
- 资源分配单元:操作系统为每个进程分配资源,如内存、文件句柄等。
- 隔离性:进程之间的通信需要借助操作系统提供的进程间通信(IPC)机制,如管道、消息队列、共享内存等。 一个进程的错误不会影响其他进程的稳定性。
管道:
管道是操作系统中最古老、最基础的 IPC 机制,本质是一个在内核中创建的临时缓冲区,用于实现两个进程之间的单向数据传输。它的工作方式类似日常生活中的 “水管”:一个进程作为 “写入端” 向管道发送数据,另一个进程作为 “读取端” 从管道接收数据,数据按照先进先出(FIFO)的顺序流动。
消息队列:
消息队列是一种更高级的 IPC 机制,它在 kernel 中创建一个具有标识符的消息链表,允许进程按 “消息” 为单位发送和接收数据。与管道的字节流不同,消息队列中的每个数据单元是结构化的 “消息”,包含消息类型和数据内容两部分,接收进程可根据消息类型选择性读取,实现按类型分拣数据的功能。 类似邮局的邮件处理。
- 管道适合简单的单向数据传输,如命令行管道、父子进程的临时数据交换,尤其适合传输连续的字节流(如日志输出、数据备份);
- 消息队列适合需要按类型区分数据、异步通信或跨无亲缘关系进程的场景,如分布式系统中的模块协作、多任务调度系统的任务分发等。
1.3 进程的状态
进程通常有以下几种状态:
- 创建(New):进程刚被创建,由于其他进程正占有CPU所以得不到执行,只能处于初始状态。
- 就绪(Ready):只有处于就绪状态的经过调度才能到执行状态。
- 运行(Running):进程正在执行。
- 阻塞(Blocked):进程等待某个事件(如I/O操作)完成。这时,即使给它CPU控制权,它也无法运行。
- 终止(Terminated):进程执行完毕或被强制结束。
二、什么是线程?
2.1 定义
线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
一个进程可以包含多个线程,它们共享进程的资源,如内存、文件句柄等,但有自己的栈和寄存器,这样可以确保线程的控制流是相对独立的。
2.2 线程的优缺点
①优点:
- 一个进程中可以同时存在多个线程
- 让进程具备多任务并行处理能力
- 同进程下的各个线程之间可以共享进程资源 (同进程内的多线程通信十分简单高效)
- 更轻量与高效
②缺点:
- 因为进程资源共享,所以会产生资源竞争,需要通过锁机制来协同
- 当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃(一般游戏的用户设计不会采用多线程方式)
2.3 进程和线程之间的区别和联系
①区别:
-
调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
-
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
- 资源:进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
-
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。进程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
②联系:
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
-
资源分配给进程,同一进程的所有线程共享该进程的所有资源;
-
处理机分给线程,即真正在处理机上运行的是线程;
-
线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
三、什么是协程?
3.1 定义
协程,又称微线程或纤程,是一种用户态的轻量级线程,完全由用户程序(而非操作系统内核)控制调度。它不像进程那样拥有独立的内存空间,也不像线程那样需要操作系统进行调度切换。
它依赖 “协作式调度” 机制:一个协程需要主动通过yield
、await
等关键字让出 CPU,其他协程才能获得执行机会。
协程通常运行在单一线程内,共享该线程的内存资源,因此创建和切换成本极低(无需内核态上下文切换),适合在有限资源下实现高并发(如单线程内创建数万协程)。
3.2 与多线程相比,协程的优势
①极高的执行效率:
因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
②不需要多线程的锁机制:
因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
3.3 与虚拟线程的关键区别
维度 | 协程(Coroutine) | 虚拟线程(Virtual Thread) |
---|---|---|
调度机制 | 协作式调度:需主动yield /await 让出 CPU | 抢占式调度:JVM 可强制切换虚拟线程(类似 OS 线程) |
与线程的关系 | 运行在单一线程内,共享线程资源 | 依赖载体线程(OS 线程)执行,可动态绑定不同载体线程 |
编程模型 | 需显式使用async/await 等语法标记切换点 | 兼容传统线程 API(如Runnable 、Thread ),无需修改代码 |
语言依赖 | 依赖编程语言原生支持(如 Python、Go、Kotlin) | 主要由 Java JVM 实现,是 JVM 层面的抽象 |
阻塞处理 | 若协程内存在阻塞调用(如同步 IO),会阻塞整个线程 | 虚拟线程的阻塞会被 JVM “翻译” 为非阻塞操作,不阻塞载体线程 |
3.4 代码示例(python)
import asyncio# 定义第一个协程函数
async def coroutine_1():for i in range(5):print(f"协程1: 输出 {i}")# 让出控制权,允许其他协程执行await asyncio.sleep(0.2)# 定义第二个协程函数
async def coroutine_2():for i in range(5):print(f"协程2: 输出 {i}")# 让出控制权,允许其他协程执行await asyncio.sleep(0.1)# 主函数,并发运行两个协程async def main():# 创建两个协程任务task1 = asyncio.create_task(coroutine_1())task2 = asyncio.create_task(coroutine_2())# 等待两个任务都完成await asyncio.gather(task1, task2)# 运行主函数
asyncio.run(main())
await asyncio.sleep(0.1)
让出 CPU 控制权
asyncio.create_task()
创建可并发执行的任务
asyncio.gather()
等待所有任务完成
协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。