python异步编程 -什么是python的异步编程, 与多线程和多进程的区别

什么是异步执行

核心思想: 异步编程是一种编程模式,它允许一个操作"在后台"启动,而无需等待该操作完成,当前线程可以立即去执行其他任务。当那个后台操作完成后,再通过回调、承诺或事件等方式通知程序,并处理其结果。

厨师的例子

假如一个厨师需要做 3道菜, 分别是褒汤(1小时), 用烤箱土豆(40分钟),炒饭(10分钟)

同步方式

厨师严格按照顺序做菜,做完一道再做下一道。

  1. 开始煲汤,花费 1 小时。
  2. 汤煲好后,开始烤土豆,花费 40 分钟。
  3. 土豆烤好后,开始炒饭,花费 10 分钟。
    总耗时:1 小时 + 40 分钟 + 10 分钟 = 1 小时 50 分钟。

注: 同步方式下, 厨师每开始一个任务后都是阻塞状态, 也就是讲必须做完一件再开始另一个任务, 这也是python 程序的默认处理方式。

异步方式

厨师可以同时处理多个任务,把需要长时间等待的任务先启动起来,然后利用等待时间去做其他事情。

  1. 先把汤煲上,这个任务需要 1 小时,但不需要一直盯着。
  2. 在煲汤的同时,把土豆放进烤箱,这个任务需要 40 分钟。
  3. 在等待汤和土豆的同时,开始炒饭,10 分钟后炒饭完成。
  4. 厨师可以休息或准备其他事情,直到土豆和汤完成。
    总耗时:取决于耗时最长的任务,即 1 小时。

注: 异步方式下, 任务分为非阻塞的(褒汤, 烤土豆 )和阻塞的(炒饭), 其实异步方式对多个非阻塞的任务很有效, 但是对阻塞性的任务帮助不大。

多线程方式

可以想象成一个厨房里(一个进程),有一个总指挥厨师(主线程)和几个助手厨师(子线程)。他们共享厨房里的工具(内存、资源)。

  1. 总指挥厨师让助手A去煲汤。
  2. 总指挥厨师让助手B去烤土豆。
  3. 总指挥厨师自己去炒饭。
    大家在同一个厨房里同时开工,提高了效率,但需要小心协调,避免互相干扰(比如资源竞争)。
    总耗时:也取决于最长的任务,即 1 小时,但会有一些协调(线程切换)的开销。

多进程方式

可以想象成雇佣了三个厨师,每人一个独立的厨房(独立的进程),每个厨房的资源(锅、灶台)都是独立的。

  1. 厨师A在厨房A里煲汤。
  2. 厨师B在厨房B里烤土豆。
  3. 厨师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 对象,并告诉它要执行哪个函数。

python 复制代码
import multiprocessing
import time
import os

def 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 类可以帮助我们方便地管理一个进程池。

python 复制代码
import multiprocessing
import time

def square(x):
    """计算一个数的平方"""
    time.sleep(1)
    return x * x

if __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 中选择正确的并发模型是优化程序性能的关键。通过本文的介绍,我们可以得出一个清晰的选择指南:

  1. 异步编程 (AsyncIO):

    • 核心优势: 在单线程内实现高并发,资源开销极低。
    • 最佳场景 : I/O 密集型任务,如大量的网络请求、数据库连接、文件读写等。当程序大部分时间在"等待"时,异步是最高效的选择。
    • 不适用: CPU 密集型任务。
  2. 多线程 (Threading):

    • 核心优势: 应对阻塞性 I/O,防止程序假死。API 相对简单。
    • 最佳场景 : I/O 密集型任务,特别是当需要与一些不支持异步的旧版库交互时。
    • 局限 : 受 GIL 限制,无法利用多核 CPU 进行并行计算,不适用于 CPU 密集型任务。
  3. 多进程 (Multiprocessing):

    • 核心优势 : 绕过 GIL,能够充分利用多核 CPU 实现真正的并行计算。
    • 最佳场景 : CPU 密集型任务,如复杂的科学计算、数据处理、视频编码等。
    • 缺点: 资源开销最大,进程间通信比线程复杂。

最终决策流程

  • 你的任务是计算密集型吗?
    • : 毫不犹豫地选择 multiprocessing
  • 你的任务是 I/O 密集型吗?
    • :
      • 优先选择 异步编程,因为它性能更高,资源占用更少。
      • 如果代码中包含无法改为异步的阻塞库,或者项目逻辑相对简单,可以考虑使用多线程
相关推荐
~~李木子~~3 小时前
用 Python 实现 Gini 决策树分类与可视化-机器学习
python·决策树·机器学习
小黄人软件3 小时前
用AI写的【实时文件搜索引擎】python源码【找资源】
开发语言·python·搜索引擎
原小明4 小时前
【Conda】Conda虚拟环境配置系统环境变量,Jupter可使用
vscode·python·jupyter·conda
B站计算机毕业设计之家4 小时前
深度学习实战:Python水果识别 CNN算法 卷积神经网络(TensorFlow训练+Django网页源码)✅
python·深度学习·神经网络·cnn·tensorflow·水果识别·识别系统
Python极客之家4 小时前
基于数据挖掘的银行贷款审批预测系统
人工智能·python·机器学习·数据挖掘·毕业设计
zhz52144 小时前
ArcGIS Pro 进程管理:自动化解决方案与最佳实践
运维·python·arcgis·自动化
kida_yuan4 小时前
【从零开始】16. 基于 CPU 的转换、量化实现
python·llm
fly五行4 小时前
大模型基础入门与 RAG 实战:从理论到 llama-index 项目搭建(有具体代码示例)
python·ai·llama·llamaindex
E_ICEBLUE5 小时前
Python 处理 Word 文档中的批注(添加、删除)
开发语言·python·microsoft·word