python异步编程 -什么是python的异步编程, 与多线程和多进程的区别
什么是异步执行
核心思想: 异步编程是一种编程模式,它允许一个操作“在后台”启动,而无需等待该操作完成,当前线程可以立即去执行其他任务。当那个后台操作完成后,再通过回调、承诺或事件等方式通知程序,并处理其结果。
厨师的例子
假如一个厨师需要做 3道菜, 分别是褒汤(1小时), 用烤箱土豆(40分钟),炒饭(10分钟)
同步方式
厨师严格按照顺序做菜,做完一道再做下一道。
- 开始煲汤,花费 1 小时。
- 汤煲好后,开始烤土豆,花费 40 分钟。
- 土豆烤好后,开始炒饭,花费 10 分钟。
总耗时:1 小时 + 40 分钟 + 10 分钟 = 1 小时 50 分钟。
注: 同步方式下, 厨师每开始一个任务后都是阻塞状态, 也就是讲必须做完一件再开始另一个任务, 这也是python 程序的默认处理方式。
异步方式
厨师可以同时处理多个任务,把需要长时间等待的任务先启动起来,然后利用等待时间去做其他事情。
- 先把汤煲上,这个任务需要 1 小时,但不需要一直盯着。
- 在煲汤的同时,把土豆放进烤箱,这个任务需要 40 分钟。
- 在等待汤和土豆的同时,开始炒饭,10 分钟后炒饭完成。
- 厨师可以休息或准备其他事情,直到土豆和汤完成。
总耗时:取决于耗时最长的任务,即 1 小时。
注: 异步方式下, 任务分为非阻塞的(褒汤, 烤土豆 )和阻塞的(炒饭), 其实异步方式对多个非阻塞的任务很有效, 但是对阻塞性的任务帮助不大。
多线程方式
可以想象成一个厨房里(一个进程),有一个总指挥厨师(主线程)和几个助手厨师(子线程)。他们共享厨房里的工具(内存、资源)。
- 总指挥厨师让助手A去煲汤。
- 总指挥厨师让助手B去烤土豆。
- 总指挥厨师自己去炒饭。
大家在同一个厨房里同时开工,提高了效率,但需要小心协调,避免互相干扰(比如资源竞争)。
总耗时:也取决于最长的任务,即 1 小时,但会有一些协调(线程切换)的开销。
多进程方式
可以想象成雇佣了三个厨师,每人一个独立的厨房(独立的进程),每个厨房的资源(锅、灶台)都是独立的。
- 厨师A在厨房A里煲汤。
- 厨师B在厨房B里烤土豆。
- 厨师C在厨房C里炒饭。
他们之间完全独立,互不干扰。这种方式资源隔离性最好,但开销也最大(相当于建了三个厨房)。
总耗时:同样取决于最长的任务,即 1 小时,但前期准备(创建进程)的开销较大。
阻塞性任务和非阻塞性任务
在编程中,任务可以根据其执行方式分为两类:阻塞性任务和非阻塞性任务。
阻塞性任务 (Blocking Tasks)
定义: 阻塞性任务是指在任务完成之前,会“阻塞”当前程序的执行流程,程序会一直等待,直到该任务返回结果。在等待期间,程序无法执行任何其他操作。
特点:
- 同步执行: 任务按顺序依次执行。
- 资源占用: 在等待 I/O 操作(如读写文件、网络请求)或长时间计算时,会占用 CPU 时间片,即使它大部分时间在等待。
例子:
- CPU 密集型任务: 大量复杂的数学计算、图像处理、数据加密等。
- 同步的 I/O 操作:
time.sleep()
、不使用异步库的网络请求、读取大文件等。
回到厨师的例子,炒饭可以看作一个阻塞性任务。厨师必须站在锅前不停地翻炒,直到炒饭完成,这个过程他无法离开去做别的事情。
非阻塞性任务 (Non-blocking Tasks)
定义: 非阻塞性任务是指启动后会立即返回,不会阻塞当前程序的执行。程序可以继续执行其他任务,而该任务在后台运行。当它完成后,会通过某种机制(如回调函数、Promise、事件循环)通知程序。
特点:
- 异步执行: 允许并发处理多个任务。
- 高效利用资源: 特别适合 I/O 密集型任务。在等待数据返回时,CPU 可以切换去处理其他任务,而不是空等。
例子:
- I/O 密集型任务: 网络请求、数据库查询、文件读写等。当这些任务使用异步库(如 Python 的
asyncio
、JavaScript 的async/await
)执行时,它们就是非阻塞的。
在厨师的例子中,煲汤和烤土豆就是典型的非阻塞性任务。厨师只需要把食材放进锅里或烤箱,设置好时间,然后就可以离开去做其他事情(比如炒饭),只需要偶尔回来检查一下状态即可。
所以
特性 | 异步编程 (Async) | 多线程 (Multi-threading) |
---|---|---|
并发模型 | 协作式多任务,单线程内通过事件循环切换 | 抢占式多任务,操作系统强制切换线程 |
资源开销 | 非常低,仅需一个线程 | 较高,每个线程都有独立的堆栈和上下文 |
CPU 利用 | 无法利用多核处理单个任务 | 可以利用多核并行执行任务 |
适用场景 | I/O 密集型,高并发,大量等待 | CPU 密集型,需要并行计算 |
数据共享 | 默认安全(单线程),无需加锁 | 复杂,需要使用锁等同步机制来避免竞争 |
编程难度 | 相对较高,需要理解事件循环和回调 | 极高,调试困难,容易出现死锁等问题 |
切换开销 | 非常小,只是函数调用 | 较大,涉及内核态和用户态的切换 |
总结:
- 当你的任务主要是等待(如等待网络响应)时,选择异步编程。
- 当你的任务主要是计算(需要多核并行处理)时,选择多线程或多进程。
什么是python的GIL锁
GIL是CPython解释器中的一个互斥锁
它防止多个线程同时执行Python字节码
这意味着在任何时刻,只有一个线程可以执行Python代码
GIL的影响:
CPU密集型任务:多线程无法利用多核CPU实现并行计算
I/O密集型任务:在等待I/O时,线程会释放GIL,其他线程可以运行
所以GIL的存在让python无法真正地多线程执行任务
虽然python也提供多线程的类, 但是实际执行效果并不佳
python的多进程编程
由于 GIL 的存在,Python 的多线程在 CPU 密集型任务上无法发挥多核优势。为了真正实现并行计算,Python 提供了 multiprocessing
模块,它允许我们创建多个进程来执行任务。
为什么使用多进程?
- 绕过 GIL: 每个进程都有自己独立的 Python 解释器和内存空间,因此每个进程都有自己的 GIL。这使得多进程可以真正地在多个 CPU 核心上并行执行代码。
- 利用多核 CPU: 对于 CPU 密集型任务(如科学计算、数据分析、图像处理),多进程是提升性能的关键。
- 稳定性: 进程之间相互独立,一个进程的崩溃不会影响其他进程,主进程也可以更好地管理子进程。
multiprocessing
模块的基本使用
Python 的 multiprocessing
模块提供了与 threading
模块相似的 API,使其易于上手。
1. 使用 Process
类
你可以创建一个 Process
对象,并告诉它要执行哪个函数。
import multiprocessing
import time
import osdef worker(num):"""子进程要执行的工作"""print(f"Worker {num} started, PID: {os.getpid()}")time.sleep(2)print(f"Worker {num} finished")if __name__ == "__main__":processes = []for i in range(5):# 创建一个进程p = multiprocessing.Process(target=worker, args=(i,))processes.append(p)# 启动进程p.start()for p in processes:# 等待所有子进程完成p.join()print("All processes finished.")
注意: 在 Windows 和 macOS 上,创建子进程的代码必须放在 if __name__ == "__main__":
块中,以避免无限递归地创建子进程。
2. 使用 Pool
(进程池)
当需要处理大量任务时,手动创建和管理进程会很繁琐。Pool
类可以帮助我们方便地管理一个进程池。
import multiprocessing
import timedef square(x):"""计算一个数的平方"""time.sleep(1)return x * xif __name__ == "__main__":# 创建一个包含4个进程的进程池with multiprocessing.Pool(processes=4) as pool:numbers = [1, 2, 3, 4, 5, 6, 7, 8]# 使用 map 方法将任务分配给进程池# 它会阻塞直到所有结果都准备好results = pool.map(square, numbers)print("Results:", results)print("All tasks finished.")
进程间通信 (IPC)
由于进程拥有独立的内存空间,它们之间不能像线程一样直接共享数据。multiprocessing
模块提供了多种进程间通信(Inter-Process Communication, IPC)的机制:
Queue
: 一个线程和进程安全的队列,允许在多个进程之间传递消息。Pipe
: 创建一个管道,返回一对连接对象,代表管道的两端。Manager
: 支持更多的数据类型,如 list, dict, Namespace 等,允许在不同进程间共享状态。
多进程的缺点
- 资源开销大: 创建和维护进程的开销远大于线程。每个进程都需要独立的内存空间,会消耗更多内存。
- 通信复杂: 进程间通信比线程间共享数据要慢,且需要显式处理。
总结
- 多进程是解决 Python 中 CPU 密集型任务并行计算的最佳选择。
- 它通过创建独立的进程来绕过 GIL 的限制,充分利用多核 CPU。
- 使用
multiprocessing
模块可以方便地创建和管理进程,但需要注意其资源开销和通信成本。
全文总结:如何选择正确的并发模型?
在 Python 中选择正确的并发模型是优化程序性能的关键。通过本文的介绍,我们可以得出一个清晰的选择指南:
-
异步编程 (AsyncIO):
- 核心优势: 在单线程内实现高并发,资源开销极低。
- 最佳场景: I/O 密集型任务,如大量的网络请求、数据库连接、文件读写等。当程序大部分时间在“等待”时,异步是最高效的选择。
- 不适用: CPU 密集型任务。
-
多线程 (Threading):
- 核心优势: 应对阻塞性 I/O,防止程序假死。API 相对简单。
- 最佳场景: I/O 密集型任务,特别是当需要与一些不支持异步的旧版库交互时。
- 局限: 受 GIL 限制,无法利用多核 CPU 进行并行计算,不适用于 CPU 密集型任务。
-
多进程 (Multiprocessing):
- 核心优势: 绕过 GIL,能够充分利用多核 CPU 实现真正的并行计算。
- 最佳场景: CPU 密集型任务,如复杂的科学计算、数据处理、视频编码等。
- 缺点: 资源开销最大,进程间通信比线程复杂。
最终决策流程
- 你的任务是计算密集型吗?
- 是: 毫不犹豫地选择
multiprocessing
。
- 是: 毫不犹豫地选择
- 你的任务是 I/O 密集型吗?
- 是:
- 优先选择 异步编程,因为它性能更高,资源占用更少。
- 如果代码中包含无法改为异步的阻塞库,或者项目逻辑相对简单,可以考虑使用多线程。
- 是: