python进程,线程与协程
一、多任务的概念
什么是多任务:多任务是指在同⼀时间内执⾏多个任务
我们之前所写的程序都是单任务的,也就是说⼀个函数或者⽅法执⾏完成 , 另外⼀个函 数或者⽅法才能执⾏。要想实现多个任务同时执⾏就需要使⽤多任务。
多任务的最⼤好处是充分利 ⽤CPU资源,提⾼程序的执⾏效率
多任务的两种表现形式:
① 并发 ② 并⾏
二、并发与并行
1、并发
概念:在⼀段时间内交替去执⾏多个任务。
- 多个任务在同一个时间段内交替执行
- 在单核CPU上通过时间片轮转实现任务切换
- 强调任务的分解和调度
例如:对于单核cpu处理多任务,操作系统轮流让各个任务交替执⾏,假如:软件1执⾏0.01秒,切换 到软件2,软件2执⾏0.01秒,再切换到软件3,执⾏0.01秒…… 这样反复执⾏下去, 实际上每个软 件都是交替执⾏的 . 但是,由于CPU的执⾏速度实在是太快了,表⾯上我们感觉就像这些软件都 在同时执⾏⼀样 . 这⾥需要注意单核cpu是并发的执⾏多任务的。
并发:任务数量大于CPU核心数
2、并⾏
概念:在⼀段时间内真正的同时⼀起执⾏多个任务。
- 多个任务在同一时刻真正同时执行
- 需要多核CPU支持
- 强调任务的同时执行
对于多核cpu处理多任务,操作系统会给cpu的每个内核安排⼀个执⾏的任务,多个内核是真正的 ⼀起同时执⾏多个任务。这⾥需要注意多核cpu是并⾏的执⾏多任务,始终有多个任务⼀起执⾏。
并行:任务数量小于或等于CPU核心数
3、并发与并行的区别
特性 | 并发 | 并行 |
---|---|---|
核心定义 | 多个任务在重叠的时间段内交替执行 | 多个任务在同一时刻同时执行 |
关键思想 | 任务切换 | 同时执行 |
硬件需求 | 单核CPU即可实现 | 必须依赖多核CPU或多台计算机 |
关注点 | 程序的设计与结构(处理多个任务的能力) | 程序的执行与计算(加快任务完成的速度) |
主要目的 | 提高系统的资源利用率和响应能力 | 提高系统的计算速度和吞吐量 |
关系 | 并发是并行的必要条件,但并发不一定并行。 并行是并发的真子集。 | |
生活比喻 | 一个人(单核)同时做两件事: 边看电视边回微信,大脑在快速切换。 | 两个人(多核)同时做两件事: 一个人看电视,另一个人同时回微信。 |
编程模型 | 多线程、协程、异步IO、事件循环 | 多进程、GPU计算、分布式计算 |
优势 | 使I/O密集型应用不会阻塞,最大化利用CPU时间片 | 充分利用多核资源,大幅缩短计算密集型任务的时间 |
挑战 | 线程安全、竞态条件、死锁、数据同步 | 任务拆分、负载均衡、进程间通信、数据一致性 |
三、进程与多进程
1、程序中实现多任务的⽅式
在Python中,想要实现多任务可以使⽤ 多进程来完成。
2、进程的概念
进程(Process)是资源分配的最⼩单位,它是操作系统进⾏资源分配和调度运⾏的基本单位,通 俗理解:⼀个正在运⾏的程序就是⼀个进程。
例如:正在运⾏的qq , 微信等 他们都是⼀个进程。
注: ⼀个程序运⾏后⾄少有⼀个进程
- 程序在执行时的一个实例(应用程序)
- 拥有独立的内存空间和系统资源
- 进程间相互隔离,安全性高
3、多进程的作⽤
图中是⼀个⾮常简单的程序 , ⼀旦运⾏hello.py这个程序 , 按照代码的执⾏顺序 , func_a函数执⾏ 完毕后才能执⾏func_b函数 . 如果可以让func_a和func_b同时运⾏ , 显然执⾏hello.py这个程序的 效率会⼤⼤提升 .
未使用多进程
使用了多进程
4、多进程完成多任务
① 导⼊进程包
import multiprocessing
② 通过进程类创建进程对象
进程对象 = multiprocessing.Process()
③ 启动进程执⾏任务
进程对象.start()
5、通过进程类创建进程对象
进程对象 = multiprocessing.Process([group [, target=任务名 [, name]]])
参数说明:
参数名 | 说明 |
---|---|
target | 执行的目标任务名,这里指的是函数名(方法名) |
name | 进程名,一般不用设置 |
group | 进程组,目前只能使用None |
6、进程创建与启动的代码
边听音乐,边敲代码:
# 导入多进程模块,用于创建和管理进程
import multiprocessing
# 导入时间模块,用于模拟耗时操作和计时
import timedef music():"""任务函数:模拟听音乐"""for i in range(3):print('听音乐...')time.sleep(0.2) # 模拟每次听音乐的耗时def coding():"""任务函数:模拟敲代码"""for i in range(3):print('敲代码...')time.sleep(0.2) # 模拟每次敲代码的耗时if __name__ == '__main__':# 创建执行music函数的进程music_process = multiprocessing.Process(target=music)# 创建执行coding函数的进程coding_process = multiprocessing.Process(target=coding)# 启动两个进程music_process.start()coding_process.start()# 等待两个进程都执行完毕music_process.join()coding_process.join()print("所有任务完成!")
7、进程执⾏带有参数的任务
Process([group [, target [, name [, args [, kwargs]]]]])
参数说明:
参数名 | 说明 | 示例 |
---|---|---|
args | 以元组的方式给执行任务传参,表示调用对象的位置参数 | args=(1, 2, 'anne') |
kwargs | 以字典方式给执行任务传参,表示调用对象的关键字参数 | kwargs={'name': 'anne', 'age': 18} |
案例:args参数和kwargs参数的使⽤
# 导入多进程模块
import multiprocessing
# 导入时间模块
import timedef music(num):"""模拟听音乐的任务参数:- num: int, 音乐播放的次数该函数通过循环模拟播放音乐,每次播放后暂停0.2秒。"""for i in range(num):print('听音乐...')time.sleep(0.2)def coding(count):"""模拟编码任务参数:- count: int, 敲代码的次数该函数通过循环模拟敲代码的动作,每次敲代码后暂停0.2秒。"""for i in range(count):print('敲代码...')time.sleep(0.2)# 在主程序中启动多进程
if __name__ == '__main__':# 创建音乐进程,参数为播放音乐的次数music_process = multiprocessing.Process(target=music, args=(3,))# 创建编码进程,参数为敲代码的次数coding_process = multiprocessing.Process(target=coding, kwargs={'count': 3})# 启动音乐进程music_process.start()# 启动编码进程coding_process.start()# 打印主进程开始的信息print('主进程开始')# 使主进程暂停1秒,以模拟进程初始化或资源准备的时间time.sleep(1)# 等待音乐进程结束music_process.join()# 等待编码进程结束coding_process.join()# 打印主进程结束的信息print('主进程结束')
注意点:
1. 入口点保护:必须使用 `if __name__ == '__main__':` 保护主程序入口
2. 参数序列化:传递给进程的参数必须是可序列化的
3. 进程间通信:使用 `Queue`、`Pipe` 等机制进行进程间数据交换
4. 资源管理:及时调用 `join()` 等待进程结束,避免僵尸进程
四、线程与多线程
1、线程的概念
在Python中,想要实现多任务还可以使⽤多线程来完成。
- 进程内的执行单元(函数,class类)
- 同一进程内的线程共享内存空间(数据可以共享)
- 是CPU调度的基本单位
2、为什么使⽤多线程?
进程是分配资源的最⼩单位 , ⼀旦创建⼀个进程就会分配⼀定的资源 , 就像跟两个⼈聊QQ就需要 打开两个QQ软件⼀样是⽐较浪费资源的 .
线程是程序执⾏的最⼩单位 , 实际上进程只负责分配资源 , ⽽利⽤这些资源执⾏程序的是线程 , 也 就说进程是线程的容器 , ⼀个进程中最少有⼀个线程来负责执⾏程序 。
同时线程⾃⼰不拥有系统 资源,只需要⼀点⼉在运⾏中必不可少的资源,但它可与同属⼀个进程的其它线程共享进程所拥有 的全部资源 。这就像通过⼀个QQ软件(⼀个进程)打开两个窗⼝(两个线程)跟两个⼈聊天⼀样 , 实 现多任务的同时也节省了资源
3、多线程完成多任务
多线程完成多任务
① 导⼊线程模块
import threading
② 通过线程类创建线程对象
线程对象 = threading.Thread(target=任务名)
③ 启动线程执⾏任务
线程对象.start()
参数名 | 说明 |
---|---|
target | 执行的目标任务名,这里指的是函数名(方法名) |
name | 线程名,一般不用设置 |
group | 线程组,目前只能使用None |
线程创建与启动代码
单线程案例
# 单线程案例
import timedef music():for i in range(3):print('听音乐...')time.sleep(0.2)def coding():for i in range(3):print('敲代码...')time.sleep(0.2)if __name__ == '__main__':music()coding()
多线程案例
# 多线程案例
import time
import threadingdef music():"""模拟听音乐的任务循环三次,每次打印听音乐的状态并暂停0.2秒"""for i in range(3):print('听音乐...')time.sleep(0.2)def coding():"""模拟编码的任务循环三次,每次打印敲代码的状态并暂停0.2秒"""for i in range(3):print('敲代码...')time.sleep(0.2)if __name__ == '__main__':# 创建一个线程,用于播放音乐。通过threading.Thread类创建线程,music函数作为线程要执行的目标。music_thread = threading.Thread(target=music)# 创建一个线程,用于编码工作。同样使用threading.Thread类创建,coding函数作为线程要执行的目标。coding_thread = threading.Thread(target=coding)# 启动音乐线程和编码线程music_thread.start()coding_thread.start()# 等待音乐线程完成其任务music_thread.join()# 等待编码线程完成其任务coding_thread.join()print('程序结束')
线程执⾏带有参数的任务
参数名 | 说明 |
---|---|
args | 以元组的方式给执行任务传参 |
kwargs | 以字典方式给执行任务传参 |
# 导入time模块和threading模块
import time
import threadingdef music(num):"""模拟听音乐的函数参数:- num: int, 控制音乐播放的次数该函数通过循环播放音乐,并在每次播放时暂停0.2秒来模拟听音乐的行为。"""for i in range(num):print('听音乐...')time.sleep(0.2)def coding(count):"""模拟编码的函数参数:- count: int, 控制编码操作的次数该函数通过循环模拟编码行为,并在每次编码时暂停0.2秒来模拟实际编码过程。"""for i in range(count):print('敲代码...')time.sleep(0.2)# 程序入口
if __name__ == '__main__':# 创建一个线程,负责播放音乐# 参数说明:# - target: 指定要执行的函数,这里是音乐播放函数# - args: 传递给函数的参数,这里使用元组(3,)作为示例music_thread = threading.Thread(target=music, args=(3,))# 创建另一个线程,负责编码任务# 参数说明:# - target: 指定要执行的函数,这里是编码函数# - kwargs: 传递给函数的关键字参数,这里指定count为3coding_thread = threading.Thread(target=coding, kwargs={'count': 3})# 启动音乐线程和编码线程music_thread.start()coding_thread.start()# 等待音乐线程完成其任务music_thread.join()# 等待编码线程完成其任务coding_thread.join()print('程序结束')
注意点
1. GIL限制:受全局解释器锁限制,CPU密集型任务无法真正并行
2. 线程安全:共享数据时需要使用锁机制保证线程安全
3. 死锁风险:多个锁的使用需要注意死锁问题
4. 资源竞争:需要合理使用同步原语(Lock、RLock、Semaphore等)
五、协程
1 协程定义与优势
协程(Coroutine)是⽤户态的轻量级线程,通过协作式多任务实现并发。
相⽐线程,协程的切换⽆需操 作系统调度,仅需保存寄存器上下⽂,因此效率更⾼。
核⼼优势:
1. ⽆锁机制:避免多线程同步开销
2. ⾼并发:单线程内处理数千级I/O密集型任务(如⽹络请求)
3. 代码简洁:⽤同步语法写异步逻辑( async/await )
2 协程实现⽅式(以 asyncio 为核⼼)
基础语法
import asyncio# 定义一个异步协程函数
async def my_coroutine():"""一个简单的协程函数,用于演示asyncio库的基本使用。此函数没有参数和返回值。"""print("Start")await asyncio.sleep(1) # 非阻塞式休眠print("End")# 运行协程
asyncio.run(my_coroutine()) # Python3.7+ 推荐方式
事件循环与任务创建
import asyncio# 定义一个异步任务,该任务会在指定延迟后完成
async def task(name, delay):"""异步任务函数,模拟一个需要时间完成的任务。参数:name: 任务名称,用于标识任务。delay: 延迟时间,任务完成前需要等待的时间(秒)。"""await asyncio.sleep(delay) # 模拟任务耗时print(f"{name} completed")# 定义主协程,用于管理和执行其他协程任务
async def main():"""主协程函数,负责调度和执行多个异步任务。"""# 创建任务列表tasks = [asyncio.create_task(task("A", 2)), # 创建任务A,延迟2秒完成asyncio.create_task(task("B", 1)) # 创建任务B,延迟1秒完成]await asyncio.gather(*tasks) # 并发执行所有任务# 运行主协程
asyncio.run(main())
注:输出顺序:B → A(任务按完成时间排序)
3 核⼼模块与API
模块/方法 | 作用描述 |
---|---|
asyncio.run() | 启动事件循环的入口函数 |
asyncio.create_task() | 将协程包装为任务对象 |
asyncio.gather() | 并发执行多个协程 |
asyncio.sleep() | 非阻塞式等待(替代 time.sleep) |
asyncio.Queue | 协程安全队列,用于生产者-消费者模型 |
4 其他协程库
1. Gevent
基于 greenlet ,通过 monkey.patch_all() ⾃动切换协程:
# 导入gevent的monkey模块,用于打补丁以支持协程
from gevent import monkey# 导入gevent模块,用于创建协程
import gevent# 为所有标准模块打上补丁,使其支持协程
# 包括socket、select、thread、time等模块的阻塞操作变为非阻塞
monkey.patch_all()# 定义一个简单的任务函数,将在协程中执行
def task():print("Gevent task")# 使用列表推导式创建3个协程任务
# gevent.spawn() 创建协程实例,每个实例执行task()函数
tasks = [gevent.spawn(task) for _ in range(3)]# 一次性等待所有协程完成
# joinall()会阻塞当前协程直到所有任务完成
gevent.joinall(tasks)
2. Greenlet ⼿动切换协程:
# 导入greenlet模块,实现轻量级协程
from greenlet import greenlet# 定义第一个协程函数
def test1():print(1)gr2.switch() # 手动切换到gr2协程# 定义第二个协程函数
def test2():print(2)# test2执行完毕后自动回到主协程# 创建协程对象
gr1 = greenlet(test1) # 基于test1函数创建协程
gr2 = greenlet(test2) # 基于test2函数创建协程# 启动协程系统
gr1.switch() # 切换到gr1协程,开始执行test1函数
注意点:
1. 异步环境:协程必须在异步环境中运行
2. await关键字:调用异步函数时必须使用 `await` 关键字
3. 阻塞操作:避免在协程中使用阻塞操作,应使用异步版本
4. 异常处理:合理处理协程中的异常,避免影响整个事件循环
5. 资源管理:正确管理协程的生命周期,及时取消不需要的任务
六、进程 vs. 线程 vs. 协程 总结对比表
性 | 进程 (Process) | 线程 (Thread) | 协程 (Coroutine) |
---|---|---|---|
基本定义 | 资源分配的基本单位, 程序的一次执行实例 | CPU调度的基本单位, 进程内的一个执行流 | 用户态的轻量级线程, 由程序员控制的协作式任务 |
资源开销 | 大 (独立内存空间、PCB) | 中等 (共享内存,但有独立栈、TCB) | 极小 (通常在KB级别,无系统开销) |
切换开销 | 大 (需要切换内存空间,CPU上下文) | 中等 (需切换CPU上下文,由OS内核调度) | 极小 (用户态切换,无内核参与) |
数据共享 | 复杂 (需要IPC:队列、管道、共享内存等) | 简单 (共享进程内存,但需注意线程安全) | 极简单 (共享所有上下文变量) |
并发能力 | 利用多核CPU (真正并行) | 利用多核CPU (真正并行) | 单线程内并发 (协作式并发) |
创建数量 | 少(通常几十到上百个) | 中(通常几百到上千个) | 极多(可轻松创建数万甚至百万个) |
隔离性/安全性 | 高 (一个进程崩溃不影响其他进程) | 低 (一个线程崩溃可能导致整个进程崩溃) | 高 (一个协程异常通常不影响其他协程) |
控制者 | 操作系统内核 | 操作系统内核 | 程序员(用户态) |
适用场景 | CPU密集型任务、 需要高安全隔离的任务 | I/O密集型任务、 需要共享数据的并发任务 | 高并发I/O密集型任务、 大量网络连接、异步编程 |
编程复杂度 | 高(需处理进程间通信) | 中(需处理线程同步和锁) | 低(异步编程模型,逻辑清晰) |
Python模块 | multiprocessing | threading | asyncio |
全局解释器锁(GIL)影响 | 无影响(每个进程有独立GIL) | 受限制(同一进程线程只能有一个执行) | 受限制(在同一个线程内运行) |