深入浅出Python多线程、多进程、协程
- 多进程 (Multiprocessing): 操作系统层面的并行。每个进程有自己独立的内存空间,进程之间互不影响。适合执行 CPU 密集型 任务,可以充分利用多核 CPU。创建和销毁进程开销较大。
- 多线程 (Multithreading): 在同一个进程内创建多个执行流。线程共享进程的内存空间。适合执行 I/O 密集型 任务(如网络请求、文件读写),因为在等待 I/O 时,其他线程可以继续执行。但在 Python 中受 GIL (Global Interpreter Lock) 的限制,同一时刻只有一个线程能执行 Python 字节码,所以在 CPU 密集型任务上,多线程并不能实现真正的并行。创建和销毁线程开销比进程小。
- 协程 (Coroutines): 用户空间的协作式多任务。协程是轻量级的,由程序自身控制切换,而不是由操作系统调度。它们在一个线程内执行,通过
await或yield from主动让出控制权,允许其他协程运行。特别适合处理 大量 I/O 密集型 任务,因为切换开销非常小。协程不能利用多核 CPU 进行并行计算。
1. 多进程 (Multiprocessing)
使用 multiprocessing 模块。请注意,在 Windows 上运行多进程代码时,通常需要将主逻辑放在 if __name__ == "__main__": 块中。
1 | import multiprocessing |
运行结果分析:
进程A和进程B几乎同时开始,并且它们的PID是不同的。总耗时接近两个任务中耗时最长的那个。这表明它们是并行执行的,各自在独立的进程中运行。
2. 多线程 (Multithreading)
使用 threading 模块。线程在同一个进程内运行,共享内存。
1 | import threading |
运行结果分析:
线程A和线程B几乎同时开始,但它们的PID是相同的,因为它们在同一个进程内。总耗时接近两个任务中耗时最长的那个,因为 time.sleep() 在等待时会释放 GIL,允许其他线程运行。
3. 协程 (Coroutines) - 使用 asyncio
使用 asyncio 模块。协程是单线程的,通过事件循环 (event loop) 来调度。await 是关键,它表示当前协程暂停执行,将控制权交还给事件循环,允许事件循环去运行其他准备好的协程。
1 | import asyncio |
运行结果分析:
协程A和协程B几乎同时开始,它们的PID也是相同的(因为它们都在同一个进程的同一个线程里运行)。总耗时接近两个任务中耗时最长的那个。这是因为当协程A执行到 await asyncio.sleep(2) 时,它会将控制权交给事件循环,事件循环发现协程B已经准备好运行(它也刚启动),就会切换到协程B执行。当协程B也遇到 await asyncio.sleep(3) 时,同样让出控制权。事件循环会等待哪个协程先完成等待,然后恢复其执行。
4. 总结与对比
| 特性 | 多进程 (multiprocessing) | 多线程 (threading) | 协程 (asyncio) |
|---|---|---|---|
| 并行/并发 | 并行 (Parallelism) - 真正利用多核 | 并发 (Concurrency) - IO密集型效率高,CPU密集型受GIL限制 | 并发 (Concurrency) - 协作式,单线程内切换 |
| 资源消耗 | 重 (独立内存空间) | 轻 (共享内存空间) | 最轻 (用户空间,栈开销小) |
| 切换方式 | 操作系统调度 (抢占式) | 操作系统调度 (抢占式) | 程序控制 (await/yield from) (协作式) |
| 适用场景 | CPU 密集型任务,需要充分利用多核 | I/O 密集型任务,简单的并发需求 | 大量 I/O 密集型任务,高并发连接 |
| 数据共享 | 需要 IPC (管道、队列等),复杂 | 共享内存,需要锁等同步机制,较复杂 | 共享内存,通常通过参数传递或共享对象,需注意协程安全 |
| 错误处理 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 | 一个协程异常通常只影响自身,但未捕获可能影响事件循环 |
| Python限制 | 不受 GIL 影响 (每个进程有自己的解释器) | 受 GIL 影响 (同一时刻只有一个线程执行 Python 字节码) | 不受 GIL 直接影响 (因为只在一个线程内),但 CPU 密集型任务会阻塞整个事件循环 |
5. 协程的async await asyncio
如果没写 asyncio、await、async 会发生什么?
async、await 和 asyncio 是协程协作模型的语法糖和运行时环境,缺一不可
没写
async def:- 如果您定义一个函数,但忘了写
async def,它就是一个普通的同步函数。 - 后果:
- 无法使用
await: 如果在这个函数内部使用了await,Python 会直接报SyntaxError错误,因为await只能在async def函数内部使用。 - 无法被
await: 这个普通函数调用后直接返回结果(或抛出异常),它不是一个可等待对象,你不能在其他async def函数内部await它。
- 无法使用
- 如果您定义一个函数,但忘了写
1 | # 错误示例:在普通函数中使用 await |
没写
await:- 如果在一个
async def函数内部,调用了一个可等待对象(比如另一个协程函数返回的对象Workspace_url(...)或asyncio.sleep(...)),但忘了写await。 - 后果:
- 不会等待: 当前协程会立即继续执行,不会暂停,也不会等待那个可等待对象的结果。
- 可等待对象被忽略 (或产生警告): 调用
Workspace_url(...)或asyncio.sleep(...)会返回一个协程对象,但因为前面没有await,这个协程对象不会被提交给事件循环调度执行。它就像一个被创建出来但没有被使用的变量一样,静静地躺在那里,直到被垃圾回收。这通常会导致预期的异步操作根本没有发生。Python 解释器可能会发出一个运行时警告,提示你有一个协程从未被 awaited。这是asyncio编程中一个非常常见的错误来源。
- 如果在一个
没写
asyncio.run()(或等效的事件循环启动代码):- 如果您定义了一个顶层的
async def main()函数,但只是简单地调用main()。 - 后果:
- 异步代码不会运行: 调用
main()只会返回一个协程对象。这个协程对象包含了所有异步逻辑的代码,但它没有被提交给任何事件循环来执行。事件循环是异步代码运行所必需的“引擎”。没有启动事件循环并把顶层协程交给它,任何async def函数内部的代码(包括await调用)都不会被执行。
- 异步代码不会运行: 调用
1
2
3
4
5
6
7
8
9
10
11async def my_app():
print("我是一个异步应用")
await asyncio.sleep(1)
print("我本该运行完毕")
# 错误示例:直接调用 async 函数
# my_app() # 调用后返回一个协程对象,但不会打印任何东西
# print("程序结束") # 这句话会立即执行
# 正确做法是:
# asyncio.run(my_app()) # 启动事件循环并运行 my_app 协程- 如果您定义了一个顶层的
async def标记函数是异步的,使其调用返回协程对象。await在异步函数内部使用,标记一个暂停点,将控制权交还给事件循环,并等待一个可等待对象完成。asyncio(特别是事件循环,通过asyncio.run或其他方式启动) 是执行协程的运行时环境,它接收协程对象,调度它们的执行,并在await点之间切换。
6. 协程的create_task
怎么才能让多个协程同时开始运行,而不是一个 await 完再 await 另一个(那样是串行了)?
asyncio.create_task() 就是解决这个问题的关键函数之一。
asyncio.create_task(coro) 的作用
- 功能: 将一个协程对象 (
coro) 包装成一个Task对象,并将其安排到当前正在运行的事件循环中等待执行。 - 返回值: 返回一个
asyncio.Task对象。
Task 是什么?
asyncio.Task是asyncio提供的核心对象之一。- 可以被看作是事件循环中正在运行(或已安排好要运行)的一个协程的句柄或代表。
Task对象本身是可等待的 (awaitable)。可以await一个 Task 对象来等待它包装的协程完成并获取结果。Task对象提供了检查协程状态(是否完成、是否被取消)、获取结果、获取异常或取消协程的方法。
asyncio.create_task() vs 直接 await
直接
await一个协程调用:1
2
3
4
5
6
7
8
9
10
11
12async def main():
print("main start")
await some_async_function() # <-- 当前 main 协程会在这里暂停,直到 some_async_function 完成
print("main end")
async def some_async_function():
print("some_async_function start")
await asyncio.sleep(2)
print("some_async_function end")
# 执行顺序: main start -> some_async_function start -> 等待2秒 -> some_async_function end -> main end
# main 函数在等待 some_async_function 完成,是串行等待。直接
await意味着当前协程必须等待被await的可等待对象完成,才能继续往下执行。使用
asyncio.create_task():1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22async def main():
print("main start")
# 创建一个 Task,将 some_async_function 安排到事件循环
task = asyncio.create_task(some_async_function())
print("some_async_function 已安排为 Task,main 继续执行")
# 此时 main 协程会立即往下执行,不会等待 task 完成
# task 会在事件循环中与 main 并发运行
# 如果 main 函数不在这里 await task,它会很快运行完毕
# 为了确保 main 等待 task 真正完成,我们需要在某个地方 await task
await task # <-- main 在这里等待 task 完成
print("task 完成,main end")
async def some_async_function():
print("some_async_function start")
await asyncio.sleep(2)
print("some_async_function end")
# 执行顺序: main start -> some_async_function 已安排... -> main 继续执行 -> (事件循环切换) some_async_function start -> (事件循环切换) main 继续执行... -> (事件循环切换) 等待2秒 -> some_async_function end -> (事件循环切换) task 完成,main end
# main 和 task 是并发运行的。main 在调度 task 后没有立即阻塞,而是继续往下执行了。
# 最后的 await task 确保 main 不会在 task 完成前结束。asyncio.create_task()告诉事件循环:“这是另一个协程任务,把它加到你的待办列表里,在合适的时候运行它。” 调用create_task的协程不会暂停,它会立即返回一个Task对象,然后继续执行自己的代码。
create_task() 的典型应用场景
- 启动“后台”任务: 想让某个协程开始执行,但不关心何时完成,或者只是偶尔需要检查它的状态。比如,启动一个日志记录协程、一个监控协程等。
- 需要独立管理任务时: 可能需要获取 Task 对象来取消一个正在运行的任务 (
task.cancel()),或者检查它是否已经完成 (task.done()),或者获取结果 (task.result())。 - 与
asyncio.gather()结合使用:asyncio.gather()可以直接接受协程对象,也可以接受 Task 对象。虽然直接传协程对象更简洁,但在需要先创建 Task 对象进行一些预处理或管理时,create_task就很有用。
asyncio.gather() 与 create_task() 的配合
用 create_task 启动多个协程,然后用 asyncio.gather 一起等待它们完成。
1 | async def download_page(url): |
asyncio.create_task() 将每个 download_page(url) 协程变成了可以在事件循环中独立运行的 Task。await asyncio.gather(*tasks) 则让 main_with_gather 协程暂停,直到所有这些 Task 都完成。在这段等待期间,事件循环会负责在这些 Task 之间切换执行,从而实现并发下载。
注意: create_task() 必须在事件循环已经运行之后调用。在使用 asyncio.run(main()) 进入 main 协程时,事件循环就已经在运行了,所以可以在 main 或由 main 调用的其他协程中安全地使用 create_task。
Task 对象的一些方法
task.done(): 检查 Task 是否已完成 (包括正常完成、抛出异常或被取消)。task.result(): 获取 Task 的结果。如果在 Task 完成前调用,会抛出InvalidStateError;如果 Task 因异常完成,会重新抛出该异常。task.exception(): 获取 Task 完成时的异常。如果正常完成或未完成,返回None。task.cancel(): 请求取消 Task。Task 内部会收到一个asyncio.CancelledError异常。Task 需要自己处理这个异常来实现优雅取消。task.cancelled(): 检查 Task 是否被取消。
7. 协程通用模板
1 | # 这是一个 Python 协程 (asyncio) 的通用代码模板 |
1 | 程序开始 |
- 标题: 深入浅出Python多线程、多进程、协程
- 作者: moye
- 创建于 : 2025-05-08 01:15:09
- 更新于 : 2025-11-25 16:17:00
- 链接: https://www.kanes.top/2025/05/07/深入浅出Python多线程、多进程、协程/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。