天空大作战
94.86M · 2026-02-04
在Python开发中,我们经常会遇到需要提升程序执行效率的场景:批量爬取网页、处理海量数据、高并发接口服务……而并发编程正是解决这类问题的核心手段。
Python提供了三种主流的并发实现方式:多进程、多线程、协程,它们各自基于不同的设计原理,适用于不同的业务场景,本文将从核心原理、GIL锁影响、代码实战、场景选型四个维度,全方位解析这三种并发方式,帮你做到知其然且知其所以然。
在讲具体实现之前,必须先区分两个易混淆的概念,这是理解后续内容的基础:
GIL全局解释器锁 GIL(Global Interpreter Lock)是CPython解释器的一个核心特性,也是理解Python并发编程的关键——它是一把互斥锁,保证同一时刻只有一个线程能执行Python字节码。
一句话总结GIL:GIL锁死了单进程内的多线程并行能力,这是Python多线程在CPU密集型任务中"拉胯"的根本原因。
接下来分别讲解多进程、多线程、协程的核心原理、实现方式、优缺点,搭配极简实战代码,让你快速上手。
基于操作系统的进程(资源分配的最小单位)实现,每个进程拥有独立的内存空间、Python解释器、GIL锁,进程间相互独立,互不干扰。进程的创建、销毁、通信依赖操作系统内核,属于重量级并发,相较于其它两者来说,属于资源开销最大的模块。
Python多进程的核心模块是multiprocessing,而进程池(Pool/ThreadPoolExecutor) 是生产环境最常用的方式,其中concurrent.futures.ProcessPoolExecutor 是语法最简洁、使用最方便的进程池实现,无需手动管理进程的创建/关闭/等待,一行代码实现任务分发,是首选方案!
multiprocessing.Pool:传统进程池,功能全面;concurrent.futures.ProcessPoolExecutor:语法统一(与线程池一致),支持with上下文管理器,自动管理进程池生命周期,无需手动close/join。以"计算多组大数的阶乘"(典型CPU密集型)为例,对比单进程和最简洁的多进程池实现,代码量减少50%,无需手动管理进程池:
import time
import math
import sys
from concurrent.futures import ProcessPoolExecutor
# 解除大整数转字符串的长度限制(0表示无限制)
sys.set_int_max_str_digits(0)
# 定义CPU密集型任务:计算大数阶乘
def calc_factorial(n):
result = math.factorial(n) # 内置阶乘函数,高效计算
return f"{n}的阶乘计算完成,结果长度:{len(str(result))}"
if __name__ == "__main__":
nums = [100000, 100001, 100002, 100003] * 2 # 放大任务量,凸显效率差异
# 1. 单进程执行
start = time.time()
[print(calc_factorial(num)) for num in nums]
print(f"单进程耗时:{time.time() - start:.2f}秒n")
# 2. 多进程最简洁实现(ProcessPoolExecutor + with,推荐生产环境直接用)
start = time.time()
# with上下文管理器:自动创建/关闭进程池,无需手动close/join
# max_workers:默认等于CPU核心数,无需手动设置
with ProcessPoolExecutor() as executor:
results = executor.map(calc_factorial, nums) # 一键分发任务,按序返回结果
for res in results:
print(res)
print(f"简洁版多进程池耗时:{time.time() - start:.2f}秒")
代码亮点:
with ProcessPoolExecutor():自动管理进程池,进入上下文创建进程,退出自动关闭并等待所有进程执行完成,彻底省去手动pool.close()和pool.join();max_workers 可选:默认值为CPU核心数(os.cpu_count()),无需手动指定,完美适配多核;executor.map():语法和单进程的map完全一致,一键将任务分发到多个进程,学习成本为0;multiprocessing.Pool更友好。为了清晰区分,附上传统进程池代码,对比后更能体现ProcessPoolExecutor的简洁性:
import multiprocessing
import time
import math
import sys
sys.set_int_max_str_digits(0)
def calc_factorial(n):
result = math.factorial(n)
return f"{n}的阶乘计算完成,结果长度:{len(str(result))}"
if __name__ == "__main__":
nums = [100000, 100001, 100002, 100003]
start = time.time()
# 传统进程池:需要手动创建、close、join,步骤繁琐
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
results = pool.map(calc_factorial, nums)
pool.close() # 手动关闭,不再接受新任务
pool.join() # 手动等待所有进程完成
for res in results:
print(res)
print(f"传统多进程池耗时:{time.time() - start:.2f}秒")
结论:ProcessPoolExecutor是Python多进程的最优简洁方案,生产环境优先使用!
优点:
ProcessPoolExecutor语法极致简洁,支持上下文管理器,开发效率高;缺点:
CPU密集型任务:大数据计算、数值分析、视频编解码、机器学习模型训练、海量数据处理等需要大量CPU运算的场景。
基于操作系统的线程(CPU调度的最小单位)实现,同一进程内的所有线程共享进程的内存空间、文件句柄等资源,线程的创建、销毁、切换由操作系统内核管理,属于轻量级并发(相比进程)。 但受GIL限制,同一进程内的多线程无法实现真正并行,仅能实现并发。
threading:Python内置线程模块,提供线程创建、同步、通信等基础功能;concurrent.futures.ThreadPoolExecutor:高级线程池模块,封装了线程的创建和管理,使用更简洁,生产环境首选。多线程处理IO密集型任务 以"批量爬取网页"(典型IO密集型)为例,对比单线程和多线程效率:
import requests
import time
from concurrent.futures import ThreadPoolExecutor
# 定义IO密集型任务:爬取网页
def crawl_url(url):
try:
response = requests.get(url, timeout=5)
return f"爬取{url}成功,状态码:{response.status_code}"
except Exception as e:
return f"爬取{url}失败,错误:{str(e)}"
if __name__ == "__main__":
urls = [
"https://www.baidu.com",
"https://www.juejin.cn",
"https://www.github.com",
"https://www.python.org",
"https://www.zhihu.com"
]
# 1. 单线程执行
start = time.time()
for url in urls:
print(crawl_url(url))
print(f"单线程耗时:{time.time() - start:.2f}秒")
# 2. 多线程执行(线程池,推荐)
start = time.time()
# 创建线程池,max_workers指定线程数,IO密集型可设为CPU核心数*5~10
with ThreadPoolExecutor(max_workers=20) as executor:
# 异步执行任务
results = executor.map(crawl_url, urls)
for res in results:
print(res)
print(f"多线程耗时:{time.time() - start:.2f}秒")
优点:
缺点:
IO密集型任务:
网络爬虫、接口请求、文件读写、数据库操作、消息队列消费等大部分时间在等待IO的场景。
协程(Coroutine)又称微线程,是用户态的轻量级线程,完全由Python程序控制(用户态),而非操作系统内核。协程基于异步编程模型,通过事件循环(Event Loop) 实现任务的切换和调度,全程在单线程内执行,无内核切换开销。 协程的核心特点是非阻塞和主动让出CPU:当一个协程遇到IO操作时,会主动让出CPU执行权,让事件循环调度其他协程执行,直到IO操作完成后再恢复执行,实现单线程内的高效并发。
Python协程的实现经历了多个版本,目前3.7+推荐使用原生async/await语法,搭配asyncio模块(Python内置异步核心模块),这是最简洁、最高效的实现方式。
asyncio:Python内置异步框架,提供事件循环、协程创建、任务调度、异步IO等核心功能;async/await:协程专用语法,替代早期的yield from,让异步代码更接近同步代码的可读性;aiohttp(异步网络请求)、aiomysql(异步数据库)、aiofiles(异步文件读写)等,需搭配协程使用。协程处理高并发IO任务 以"异步爬取网页"为例,对比多线程和协程的效率(协程在超高并发IO下优势更明显):
import asyncio
import time
import aiohttp
urls = [
"https://www.baidu.com",
"https://www.juejin.cn",
"https://www.github.com",
"https://www.python.org",
"https://www.zhihu.com"
]
# 定义异步协程任务:异步爬取网页
async def crawl_url_async(session, url):
try:
async with session.get(url, timeout=5) as response:
return f"爬取{url}成功,状态码:{response.status}"
except Exception as e:
return f"爬取{url}失败,错误:{str(e)}"
# 主协程
async def main():
# 创建异步会话
async with aiohttp.ClientSession() as session:
# 创建任务列表
tasks = [asyncio.create_task(crawl_url_async(session, url)) for url in urls]
# 等待所有任务完成(并发执行)
results = await asyncio.gather(*tasks)
for res in results:
print(res)
if __name__ == "__main__":
# 1. 协程执行
start = time.time()
# 获取事件循环并运行主协程
asyncio.run(main())
print(f"协程耗时:{time.time() - start:.2f}秒")
# 对比多线程(复用3.2的代码)
from concurrent.futures import ThreadPoolExecutor
def crawl_url_sync(url):
try:
import requests
response = requests.get(url, timeout=5)
return f"爬取{url}成功,状态码:{response.status_code}"
except Exception as e:
return f"爬取{url}失败,错误:{str(e)}"
start = time.time()
with ThreadPoolExecutor(max_workers=20) as executor:
executor.map(crawl_url_sync, urls*20)
print(f"多线程耗时:{time.time() - start:.2f}秒")
优点:
缺点:
超高并发的IO密集型任务: 高并发接口服务、海量网络爬虫、实时消息推送、物联网设备数据采集等需要支撑数万级甚至十万级并发的IO场景。
| 特性 | 多进程 | 多线程 | 协程 |
|---|---|---|---|
| 核心单位 | 操作系统进程 | 操作系统线程 | 用户态微线程 |
| GIL影响 | 不受影响(多核并行) | 受影响(单进程并发) | 不受影响(单线程并发) |
| 创建/切换开销 | 极大(内核级) | 中等(内核级) | 极小(用户级) |
| 内存占用 | 高(独立内存空间) | 中(共享进程内存) | 极低(单线程内存) |
| 并发能力 | 中等(受CPU核心数限制) | 中低(受内核和GIL限制) | 极高(单线程数万级) |
| 数据共享/通信 | 复杂(管道/队列/共享内存) | 简单(共享资源,需加锁) | 极简单(单线程共享,无需加锁) |
| 线程/进程安全 | 安全(进程独立) | 不安全(需加锁) | 安全(单线程执行) |
| 代码侵入性 | 低(接近同步代码) | 低(接近同步代码) | 高(需异步语法/库) |
| 跨平台兼容性 | 一般(Windows有限制) | 好 | 好 |
核心选型原则:
根据任务类型(CPU密集/IO密集)和并发量,结合三者的特性选择,无需追求"最先进",只选"最合适"。
实际开发中,很多场景需要多进程+协程的组合,兼顾多核并行和超高并发:
threading.Lock)导致数据错乱;async函数中使用requests、pymysql等同步IO库,会阻塞协程,失去并发意义;if __name__ == "__main__":会导致进程无限创建,最终崩溃。Python的多进程、多线程、协程并非互斥关系,而是针对不同场景的互补解决方案,核心围绕GIL锁和任务类型展开:
CPU密集选多进程,普通IO密集选多线程,超高IO密集选协程;