type
status
date
slug
summary
tags
category
icon
password
comment
在 Python 中,代码默认是串行执行的,即程序会按顺序逐行运行。当遇到需要等待的任务(如文件读取、网络请求),程序会暂停执行,直到任务完成后才继续。这种方式简单直观,但当涉及大量计算或高并发 I/O 任务时,效率就会大大降低。
为了更高效地执行任务,Python 提供了三种主要的并发编程模型:
- 多线程(Threading):适用于I/O 密集型任务(如文件操作、网络请求、数据库查询),可以并发执行多个任务,但由于GIL(全局解释器锁)的存在,无法真正实现并行计算。
- 多进程(Multiprocessing):适用于CPU 密集型任务(如数据分析、科学计算),可以利用多核 CPU 实现真正的并行计算,但进程间通信开销较大。
- 异步编程(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 类来创建和管理线程。一个线程的基本生命周期包括:- 创建线程:实例化
Thread对象,指定任务函数。
- 启动线程:调用
start()方法,让线程开始执行。
- 等待线程结束:使用
join()方法,确保主程序等待线程完成后再继续执行。
- 终止线程: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 核心上同时运行多个任务,使得计算密集型任务执行速度大幅提升。🔹 代码解析
- 创建进程:
multiprocessing.Process(target=compute, args=(1,))- 这行代码创建了一个进程,执行
compute(1)。
- 启动进程:
process1.start()和process2.start()让进程真正执行。
- 等待进程完成:
join()让主进程等待子进程执行完毕后再继续运行。
🔹 if __name__ == "__main__" 的必要性
在
multiprocessing 中,建议所有进程的创建代码放在 if __name__ == "__main__" 之下,原因如下:- 在 Windows 和 macOS 上,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 进行异步任务调度
🔹 代码解析
async def定义协程:async def async_task(name)定义了一个异步函数(协程)。
await关键字:await asyncio.sleep(2)不会阻塞整个程序,而是让其他任务继续运行。
asyncio.create_task():- 并发执行多个任务,让
task1和task2几乎同时开始。
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(如网络、磁盘、数据库操作),使用
threading或asyncio。
- 如果任务主要涉及 大量 CPU 计算(如矩阵运算、图像处理),使用
multiprocessing。
- 如果需要处理 超高并发(如 10 万个网络请求),使用
asyncio。
🔹 实际应用示例
- 网络爬虫(I/O 密集) →
threading或asyncio
- 爬取 1000 个网页 →
asyncio(更高效)
- 数据分析(CPU 密集) →
multiprocessing
- 运行神经网络 →
multiprocessing
- 高并发 API 服务器 →
asyncio
✅ 选择正确的并发模型,能让 Python 代码运行得更高效! 🚀
⏭️ 下一节预告:🔍 Python 正则表达式:高效处理文本数据
在下一篇文章中,我们将深入学习 Python 正则表达式(
re 模块),帮助你掌握强大的文本匹配和处理技巧,让数据清理、日志解析、爬虫开发更加高效!🎯 你将学习:
- 正则表达式基础:常见匹配符、元字符、模式修饰符
- 字符串搜索与替换:高效查找、提取和修改文本
- 高级技巧:使用分组、非贪婪匹配、断言处理复杂文本
🍀 掌握正则表达式,让你的 Python 代码更智能、更强大! 🚀 敬请期待!