Python并发操作之多进程、多线程、协程

在Python开发中,我们经常会遇到需要提升程序执行效率的场景:批量爬取网页、处理海量数据、高并发接口服务……而并发编程正是解决这类问题的核心手段。

Python提供了三种主流的并发实现方式:多进程、多线程、协程,它们各自基于不同的设计原理,适用于不同的业务场景,本文将从核心原理GIL锁影响代码实战场景选型四个维度,全方位解析这三种并发方式,帮你做到知其然且知其所以然。

一、先搞懂核心概念:并行与并发

在讲具体实现之前,必须先区分两个易混淆的概念,这是理解后续内容的基础:

  • 并发(Concurrency):同一时间段内处理多个任务,任务交替执行(单核CPU即可实现,本质是"假同时"),比如一个厨师同时处理切菜、炒菜、盛菜,同一时间只做一件事,但快速切换。
  • 并行(Parallelism):同一时刻同时处理多个任务(必须依赖多核CPU),比如两个厨师同时切菜和炒菜,真正的"同时进行"。 Python的多进程、多线程、协程,本质上是实现并发/并行的不同技术手段,而它们的性能差异,核心受GIL全局解释器锁影响。

二、Python的"千古难题之GIL全局解释器锁"

GIL全局解释器锁 GIL(Global Interpreter Lock)是CPython解释器的一个核心特性,也是理解Python并发编程的关键——它是一把互斥锁,保证同一时刻只有一个线程能执行Python字节码

GIL的核心影响

  1. 对CPU密集型任务:多线程无法实现真正的并行,因为即使开启多个线程,也会被GIL限制为同一时刻只有一个线程在执行,反而会因为线程切换带来额外开销,导致效率甚至低于单线程。
  2. 对IO密集型任务:GIL的影响可以忽略,因为IO操作(网络请求、文件读写、数据库操作)时,线程会主动释放GIL,让其他线程有机会执行,此时多线程能有效提升效率。
  3. 多进程不受GIL影响:每个进程拥有独立的Python解释器和内存空间,各自持有一把GIL,因此多核CPU下多进程可以实现真正的并行。

一句话总结GIL:GIL锁死了单进程内的多线程并行能力,这是Python多线程在CPU密集型任务中"拉胯"的根本原因。

三、三种并发方式深度解析

接下来分别讲解多进程、多线程、协程的核心原理、实现方式、优缺点,搭配极简实战代码,让你快速上手。

3.1 多进程:突破GIL,多核并行的最佳选择

核心原理

基于操作系统的进程(资源分配的最小单位)实现,每个进程拥有独立的内存空间、Python解释器、GIL锁,进程间相互独立,互不干扰。进程的创建、销毁、通信依赖操作系统内核,属于重量级并发,相较于其它两者来说,属于资源开销最大的模块。

核心实现模块

Python多进程的核心模块是multiprocessing,而进程池(Pool/ThreadPoolExecutor) 是生产环境最常用的方式,其中concurrent.futures.ProcessPoolExecutor语法最简洁、使用最方便的进程池实现,无需手动管理进程的创建/关闭/等待,一行代码实现任务分发,是首选方案!

  • multiprocessing.Pool:传统进程池,功能全面;
  • concurrent.futures.ProcessPoolExecutor:语法统一(与线程池一致),支持with上下文管理器,自动管理进程池生命周期,无需手动close/join。
实战代码:多进程最简洁用法(ProcessPoolExecutor,推荐首选)

以"计算多组大数的阶乘"(典型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}秒")

代码亮点

  1. with ProcessPoolExecutor():自动管理进程池,进入上下文创建进程,退出自动关闭并等待所有进程执行完成,彻底省去手动pool.close()和pool.join()
  2. max_workers 可选:默认值为CPU核心数(os.cpu_count()),无需手动指定,完美适配多核;
  3. executor.map():语法和单进程的map完全一致,一键将任务分发到多个进程,学习成本为0;
  4. 跨平台兼容:Windows下无需额外处理,比multiprocessing.Pool更友好。
传统进程池(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多进程的最优简洁方案,生产环境优先使用!

优缺点

优点

  • 突破GIL限制,多核CPU下实现真正并行,CPU密集型任务效率提升显著;
  • 进程间独立,一个进程崩溃不会影响其他进程,稳定性高;
  • ProcessPoolExecutor语法极致简洁,支持上下文管理器,开发效率高;
  • 进程池复用进程,避免频繁创建/销毁进程的开销(比手动创建进程更高效)。

缺点

  • 进程创建、切换基础开销仍大于线程/协程;
  • 进程间通信复杂(需通过队列、管道等),数据共享成本高;
  • 单个进程内存占用高,进程数不宜超过CPU核心数。
适用场景

CPU密集型任务:大数据计算、数值分析、视频编解码、机器学习模型训练、海量数据处理等需要大量CPU运算的场景。

3.2 多线程:轻量并发,IO密集型任务的性价比之选

核心原理

基于操作系统的线程(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}秒")
优缺点

优点

  • 线程轻量,创建、销毁、切换开销远小于进程,内存占用低;
  • 同一进程内线程共享资源,通信简单(需注意线程安全);
  • 跨平台兼容性好,使用成本低。

缺点

  • 受GIL限制,CPU密集型任务无法提升效率,甚至可能降低效率;
  • 线程安全问题(多个线程操作共享资源时易出现竞态条件),需加锁控制,增加开发复杂度;
  • 一个线程崩溃可能导致整个进程崩溃(线程共享进程资源)。
适用场景

IO密集型任务

网络爬虫、接口请求、文件读写、数据库操作、消息队列消费等大部分时间在等待IO的场景。

3.3 协程:微线程,极致轻量的单线程并发

核心原理

协程(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}秒")
优缺点

优点

  • 极致轻量:协程创建成本极低,单线程可创建数万个协程,内存占用可忽略;
  • 无切换开销:用户态调度,无内核切换和GIL锁竞争,调度效率远高于进程/线程;
  • 高并发:单线程即可实现数万级并发,远超多线程的并发能力(多线程受内核限制,一般最多数百个);
  • 无需考虑线程安全:单线程执行,无共享资源竞争,无需加锁。

缺点

  • 仅支持IO密集型任务:单线程执行,无法利用多核CPU,CPU密集型任务会阻塞整个事件循环;
  • 代码侵入性强:需使用专门的异步语法(async/await)和异步库,同步库无法直接在协程中使用;
  • 调试难度高:异步代码的执行流程是非线性的,问题排查比同步代码更复杂;
  • 对开发者要求高:需要理解事件循环、异步调度等底层原理,避免写出"阻塞协程"的代码。
适用场景

超高并发的IO密集型任务: 高并发接口服务、海量网络爬虫、实时消息推送、物联网设备数据采集等需要支撑数万级甚至十万级并发的IO场景。

四、三者核心对比与场景选型指南

4.1 核心参数对比表

特性多进程多线程协程
核心单位操作系统进程操作系统线程用户态微线程
GIL影响不受影响(多核并行)受影响(单进程并发)不受影响(单线程并发)
创建/切换开销极大(内核级)中等(内核级)极小(用户级)
内存占用高(独立内存空间)中(共享进程内存)极低(单线程内存)
并发能力中等(受CPU核心数限制)中低(受内核和GIL限制)极高(单线程数万级)
数据共享/通信复杂(管道/队列/共享内存)简单(共享资源,需加锁)极简单(单线程共享,无需加锁)
线程/进程安全安全(进程独立)不安全(需加锁)安全(单线程执行)
代码侵入性低(接近同步代码)低(接近同步代码)高(需异步语法/库)
跨平台兼容性一般(Windows有限制)

4.2 终极选型指南

核心选型原则

根据任务类型(CPU密集/IO密集)和并发量,结合三者的特性选择,无需追求"最先进",只选"最合适"。

必选多进程的场景
  • 任务类型:CPU密集型(大数据计算、数值分析、视频编解码、模型训练);
  • 核心需求:利用多核CPU提升运算效率,追求处理速度。
必选多线程的场景
  • 任务类型:普通IO密集型(常规爬虫、接口请求、文件读写);
  • 核心需求:轻量并发,开发成本低,无需超高并发,同步库可直接使用。
必选协程的场景
  • 任务类型:超高并发IO密集型(高并发接口、海量爬虫、实时推送);
  • 核心需求:支撑数万级以上并发,追求极致的资源利用率和并发效率。
混合使用场景

实际开发中,很多场景需要多进程+协程的组合,兼顾多核并行和超高并发:

  • 原理:创建与CPU核心数相等的进程,每个进程内启动一个事件循环,运行大量协程;
  • 优势:既利用了多核CPU的并行能力,又发挥了协程的超高并发优势;
  • 适用场景:高并发服务端(如异步Web框架FastAPI/Starlette的多进程部署)、海量爬虫集群等。

五、避坑指南:并发编程的常见误区

  1. 用多线程处理CPU密集型任务:受GIL限制,效率会比单线程更低,甚至出现卡顿;
  2. 用协程处理CPU密集型任务:单线程执行,会阻塞事件循环,导致整个程序失去并发能力;
  3. 多线程未处理线程安全:多个线程操作共享资源时,未加锁(如threading.Lock)导致数据错乱;
  4. 协程中使用同步库:在async函数中使用requestspymysql等同步IO库,会阻塞协程,失去并发意义;
  5. 多进程进程数设置过大:进程数超过CPU核心数,会导致进程切换开销剧增,效率下降(建议等于CPU核心数);
  6. Windows下多进程代码未放在if __name__ == "__main__":会导致进程无限创建,最终崩溃。

六、总结

Python的多进程、多线程、协程并非互斥关系,而是针对不同场景的互补解决方案,核心围绕GIL锁任务类型展开:

  1. GIL是核心约束:决定了多线程无法用于CPU密集型任务,而多进程和协程不受此限制;
  2. 任务类型是选型依据CPU密集选多进程,普通IO密集选多线程,超高IO密集选协程
  3. 极致性能靠组合:多进程+协程是Python高并发、高性能的黄金组合,兼顾多核并行和超高并发;
  4. 开发成本需平衡:协程效率最高,但开发和调试成本也最高,普通场景下多线程的性价比更高。
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com