🚀 Python 并发编程:释放计算与 I/O 的潜能

Evan Zhou

Tech Chronicles|Feb 2, 2025|Last edited: Oct 17, 2025|
type
status
date
slug
summary
tags
category
icon
password
comment
在 Python 中,代码默认是串行执行的,即程序会按顺序逐行运行。当遇到需要等待的任务(如文件读取、网络请求),程序会暂停执行,直到任务完成后才继续。这种方式简单直观,但当涉及大量计算高并发 I/O 任务时,效率就会大大降低。
为了更高效地执行任务,Python 提供了三种主要的并发编程模型
  1. 多线程(Threading):适用于I/O 密集型任务(如文件操作、网络请求、数据库查询),可以并发执行多个任务,但由于GIL(全局解释器锁)的存在,无法真正实现并行计算
  1. 多进程(Multiprocessing):适用于CPU 密集型任务(如数据分析、科学计算),可以利用多核 CPU 实现真正的并行计算,但进程间通信开销较大
  1. 异步编程(AsyncIO):适用于超高并发的 I/O 任务(如网络爬虫、批量 API 请求),通过非阻塞机制让单线程高效执行多个任务,避免等待时间浪费。
这三种方式各有优劣,接下来,我们将逐步深入了解它们的特点和适用场景。 🚀

1. 并发 vs. 并行:核心概念解析

在深入 Python 并发编程之前,先理解几个基础概念:

🔹 什么是进程与线程?

在操作系统中:
  • 进程(Process):一个独立运行的程序,每个进程有独立的内存空间
  • 线程(Thread)进程内部的执行单元,多个线程共享进程的内存,但能独立执行不同任务
方式
特点
适用场景
单线程
任务按顺序执行
适用于小型任务或无 I/O 操作
多线程
多个任务交替运行,共享内存
适用于 I/O 任务,如爬虫、文件处理
多进程
任务并行执行,独立内存
适用于 CPU 计算,如数据分析、机器学习

🔹 串行 vs. 并发 vs. 并行


🔸 串行执行(Sequential Execution)

串行执行意味着程序按顺序逐行运行,任务之间没有重叠。例如:
执行耗时:4 秒(因为任务是依次执行的)

🔸 并发(Concurrency) vs. 并行(Parallelism)

概念
描述
适用场景
并发(Concurrency)
任务交替执行,不一定真正同时运行
适用于 I/O 任务,如下载文件、数据库查询
并行(Parallelism)
任务同时运行,需要多核 CPU
适用于 CPU 计算,如数据分析、机器学习
🔹 并发执行(多线程)
使用 threading 模块实现任务交替执行,适用于 I/O 密集型任务
执行时间 ≈ 2 秒(因为线程交替执行,但仍受 GIL 限制,无法真正并行)。 🔹 并行执行(多进程)
使用 multiprocessing 模块真正并行运行任务,适用于 CPU 密集型任务

在 Python 中:
  • 多线程 提供并发,但受 GIL 限制,无法真正并行。
  • 多进程 允许并行计算,但进程间开销较大
  • 异步编程 让单线程高效处理 I/O,适用于高并发任务。

2. 多线程(Threading):并发处理 I/O 任务

Python 多线程(Threading) 适用于 I/O 密集型任务,如文件操作、网络爬虫、数据库查询等。由于这些任务的主要瓶颈是等待时间(如磁盘、网络 I/O),而不是 CPU 计算能力,因此可以使用多线程提高程序的响应速度。

🔹 什么是多线程?

  • 线程(Thread) 是程序中的最小执行单元,它运行在进程(Process)中,多个线程可以共享同一个进程的资源。
  • Python 线程是“伪并行” 的,因为 GIL(全局解释器锁) 限制了 Python 线程无法真正并行运行 CPU 任务,但对于 I/O 任务 依然能够提高程序效率。

🔹 创建和管理线程

Python 提供 threading.Thread 类来创建和管理线程。一个线程的基本生命周期包括:
  1. 创建线程:实例化 Thread 对象,指定任务函数。
  1. 启动线程:调用 start() 方法,让线程开始执行。
  1. 等待线程结束:使用 join() 方法,确保主程序等待线程完成后再继续执行。
  1. 终止线程:Python 没有 stop() 方法,线程结束通常由任务函数自然结束或使用 daemon 守护模式。

🔹 使用 threading 运行多个任务

🔍 代码解析

  • Thread(target=task, args=("线程1",))
    • target 指定线程要执行的函数
    • args 传递给函数的参数(注意需要是元组格式)
  • start()
    • 启动线程,线程会执行 target 指定的函数。
  • join()
    • 阻塞主线程,直到子线程执行完毕,确保所有线程都完成后再继续运行主程序。
执行时间:约 2 秒(任务并发运行)

🔹 守护线程(Daemon Thread)

默认情况下,Python 线程是 非守护线程(Non-Daemon Thread),意味着:
  • 主线程必须等待所有子线程执行完毕后才能退出,即使这些子线程运行的是后台任务。
然而,在某些情况下,我们不希望主线程等待子线程,而是让主线程结束时自动终止所有子线程。这时可以使用 守护线程(Daemon Thread)

🔹 非守护线程


🔹 使用 daemon=True 设置守护线程
💡 解释
  • 由于 daemon=True,主线程退出后,子线程被立即终止,不会等子线程运行完 sleep(5)
  • 子线程不会打印 "✅ 子线程完成",因为主线程已经结束

🔹 守护线程 vs. 非守护线程 总结

功能
非守护线程(默认)
守护线程 (daemon=True)
子线程是否独立执行
是,必须执行完毕
否,主线程退出时自动终止
适用于
计算任务、文件操作、数据库操作
日志、监控、后台任务
主线程退出时的行为
等待子线程 完成
立即终止子线程
最佳实践
  • 如果任务重要,应该使用非守护线程,确保执行完毕。
  • 如果任务是后台监控类任务,可以使用守护线程,避免影响主程序的运行。

🔹 线程同步与锁(防止数据竞争)

由于多个线程共享同一块内存,如果多个线程同时修改同一个变量,可能会出现 竞态条件(Race Condition),导致数据错误。

示例:多个线程同时修改同一变量

为什么要用锁?
  • 数据竞争问题:如果多个线程同时执行 counter += 1,可能会因为 线程切换 导致部分更新丢失。
  • 解决方案:使用 threading.Lock() 确保每次只有一个线程修改共享变量

🔹 线程池(ThreadPoolExecutor)

当有 大量短小任务 需要执行时,手动创建线程 效率低且管理复杂。
Python 提供 concurrent.futures.ThreadPoolExecutor 线程池,可自动管理线程,提高性能。
线程池的优势
  • 自动管理线程:不需要手动 start()join()
  • 控制最大并发数:避免创建过多线程导致性能下降。

🔹 小结

操作
方法
说明
创建线程
threading.Thread(target=func, args=(...))
通过 Thread 创建新线程
启动线程
thread.start()
启动线程
等待线程结束
thread.join()
阻塞主线程,直到子线程完成
守护线程
thread.daemon = True
让线程随主线程退出
线程锁
threading.Lock()
防止多个线程修改同一变量
线程池
ThreadPoolExecutor(max_workers=n)
自动管理线程,提高效率

3. 多进程(Multiprocessing):适合 CPU 计算

在 Python 中,由于 GIL(全局解释器锁) 的存在,Python 的多线程无法真正实现并行计算,而 多进程(Multiprocessing) 可以让 Python 利用多个 CPU 核心 来执行任务,从而绕过 GIL 限制,实现真正的并行计算。
多进程适用于:
  • CPU 密集型任务(如数据处理、科学计算、机器学习)
  • 复杂的并行任务(如密码破解、图像处理、矩阵运算)

🔹 多线程 vs. 多进程

对比项
多线程(Threading)
多进程(Multiprocessing)
适用任务
I/O 密集型(文件读写、网络请求)
CPU 密集型(科学计算、大量数学运算)
是否并行
(受 GIL 限制)
(真正的并行计算)
资源占用
共享内存,消耗较少
进程独立,内存占用较大
启动速度
较慢,因创建新进程需要更多时间
数据共享
线程共享全局变量
进程间数据不共享,需使用 IPC 机制

🔹 使用 multiprocessing 进行并行计算

multiprocessing 允许在多个 CPU 核心上同时运行多个任务,使得计算密集型任务执行速度大幅提升。

🔹 代码解析

  1. 创建进程
      • multiprocessing.Process(target=compute, args=(1,))
      • 这行代码创建了一个进程,执行 compute(1)
  1. 启动进程
      • process1.start()process2.start() 让进程真正执行。
  1. 等待进程完成
      • join() 让主进程等待子进程执行完毕后再继续运行。

🔹 if __name__ == "__main__" 的必要性

multiprocessing 中,建议所有进程的创建代码放在 if __name__ == "__main__" 之下,原因如下:
  • WindowsmacOS 上,Python 会重新导入整个脚本,不加 if __name__ == "__main__" 可能会导致无限递归创建进程
  • 这样可以避免子进程再次执行主进程的代码

🔹 使用进程池(Pool)管理多个进程

如果有 大量任务 需要并行执行,可以使用 进程池(Pool) 自动管理多个进程,而不是手动创建多个 Process

🔹 进程池的优势

  • 自动管理进程:不需要手动 start()join()
  • 限制并发数:可以避免创建过多进程导致系统资源耗尽。

🔹 多进程间的数据通信

默认情况下,进程之间不会共享全局变量,但可以使用 队列(Queue)管道(Pipe) 在进程间传递数据。

🔹 使用 Queue 进行进程间通信

🔹 代码解析

  • multiprocessing.Queue():创建一个进程安全的队列,用于存储多个进程的计算结果。
  • q.put(result)子进程将数据存入队列
  • q.get()主进程从队列取出数据,确保结果不会丢失。

4. 异步编程(AsyncIO):高效 I/O 并发

在 Python 中,asyncio 提供了一种非阻塞 I/O 处理机制,让单线程可以高效管理多个 I/O 任务(如网络请求、数据库查询、文件读写等),避免因等待而浪费时间

适用于:
  • 网络爬虫(批量抓取网页)
  • 批量 API 请求(同时向多个 API 发送请求)
  • 实时聊天服务器(WebSocket)
  • 高并发任务调度

🔹 为什么使用 asyncio

传统的多线程虽然可以处理 I/O 任务,但线程的上下文切换存在一定开销。而 asyncio 通过事件循环(Event Loop),让程序在等待 I/O 时执行其他任务,提高执行效率。
对比项
多线程(Threading)
异步编程(AsyncIO)
并发模型
依赖多个线程
单线程事件循环
适用任务
I/O 密集型,如文件读写、网络请求
高并发 I/O,如爬虫、API 批量请求
性能
线程切换有开销
非阻塞执行,提高吞吐量
数据共享
线程间共享数据(需加锁)
依靠协程,无需加锁

🔹 使用 asyncio 进行异步任务调度

🔹 代码解析

  1. async def 定义协程
      • async def async_task(name) 定义了一个异步函数(协程)。
  1. await 关键字
      • await asyncio.sleep(2) 不会阻塞整个程序,而是让其他任务继续运行。
  1. asyncio.create_task()
      • 并发执行多个任务,让 task1task2 几乎同时开始
  1. asyncio.run(main())
      • 启动 asyncio 事件循环,运行 main()
执行时间 ≈ 2 秒(因为任务是并发执行的)。

🔹 asyncio.gather() 让多个任务并发执行

asyncio.gather()asyncio 最常用的函数之一,它可以同时运行多个异步任务
asyncio.gather() 让所有任务并发执行,最快的任务先完成

🔹 在 asyncio 中使用 async with

在文件、数据库、网络操作中,通常需要确保资源正确释放(如自动关闭文件、断开数据库连接)。
可以使用 async with 语法,例如处理文件 I/O:
async with 语法自动管理资源,即使发生异常也会确保文件正确关闭。

🔹 asyncio.Queue() 实现任务队列

在实际开发中,我们经常需要处理多个生产者和消费者,比如:
  • 爬虫任务队列
  • 批量 API 请求
  • 数据处理流水线
Python asyncio.Queue() 提供了高效的异步任务管理
生产者-消费者模型
  • 生产者 不断往 queue 里添加任务。
  • 消费者 负责从 queue 里取出任务并执行。
  • queue.task_done() 标记任务完成。

🔹 总结

  • asyncio 适用于 高并发 I/O 任务,如爬虫、API 请求、数据库查询。
  • async def 定义异步函数,await 关键字执行异步任务。
  • asyncio.gather() 让多个异步任务并发执行
  • async with 适用于文件 I/O,确保资源释放
  • asyncio.Queue() 可实现任务队列,用于生产者-消费者模型。

5. 选择合适的并发模型

在 Python 中,不同的并发模型适用于不同类型的任务。以下是针对不同任务类型的最佳选择:
任务类型
适合方案
示例
I/O 密集型(网络请求、文件读写)
threading / asyncio
爬虫、文件处理
CPU 密集型(数学计算、大数据处理)
multiprocessing
科学计算、机器学习
超大量 I/O 操作(高并发服务)
asyncio
WebSocket、爬虫

🔹 如何判断任务类型?

  • 如果任务主要涉及 等待 I/O(如网络、磁盘、数据库操作),使用 threadingasyncio
  • 如果任务主要涉及 大量 CPU 计算(如矩阵运算、图像处理),使用 multiprocessing
  • 如果需要处理 超高并发(如 10 万个网络请求),使用 asyncio

🔹 实际应用示例

  • 网络爬虫(I/O 密集)threadingasyncio
  • 爬取 1000 个网页asyncio(更高效)
  • 数据分析(CPU 密集)multiprocessing
  • 运行神经网络multiprocessing
  • 高并发 API 服务器asyncio
选择正确的并发模型,能让 Python 代码运行得更高效! 🚀

⏭️ 下一节预告:🔍 Python 正则表达式:高效处理文本数据

在下一篇文章中,我们将深入学习 Python 正则表达式(re 模块),帮助你掌握强大的文本匹配和处理技巧,让数据清理、日志解析、爬虫开发更加高效!

🎯 你将学习:

  • 正则表达式基础:常见匹配符、元字符、模式修饰符
  • 字符串搜索与替换:高效查找、提取和修改文本
  • 高级技巧:使用分组、非贪婪匹配、断言处理复杂文本
🍀 掌握正则表达式,让你的 Python 代码更智能、更强大! 🚀 敬请期待!
Loading...