Python的并发编程一直是新手和老手都容易搞混的话题。GIL、多线程、多进程、协程、异步IO……这些概念堆在一起,确实让人头大。这篇文章不扯复杂的理论,直接从实际场景出发,讲清楚什么情况该用哪种并发方案

先搞清楚:并发和并行是两码事

很多人在说"并发"的时候其实指的是"并行",这两个概念必须区分清楚。

  • 并发:多个任务交替执行,看起来像是在同时进行。单核CPU就能实现。

  • 并行:多个任务真正同时执行,需要多核CPU的支持。

举个生活中的例子:你一边吃早饭一边看手机,这其实是"并发"——你在一段时间内交替做两件事。如果要"并行",你得有两个脑子才行。

Python的GIL(全局解释器锁)导致CPython中的多线程无法实现真正的并行(同一时刻只有一个线程在执行Python字节码),但可以实现并发。这是理解Python并发编程的起点。

一、多线程:IO密集型任务的首选

Python的多线程虽然受GIL限制,但在IO密集型场景下依然是很好的选择。

什么是IO密集型任务?就是程序大部分时间在等待IO操作完成,比如:

  • 网络请求(调用API、爬虫)

  • 数据库查询

  • 文件读写

  • 用户输入

在这些场景下,线程在等待IO时会释放GIL,其他线程可以执行,所以多线程能显著提升效率。

python

import threading
import time
import requests

def fetch_url(url):
    """模拟网络请求"""
    print(f"开始请求: {url}")
    response = requests.get(url)
    print(f"完成请求: {url},状态码: {response.status_code}")
    return response.status_code

# 串行执行 — 总耗时 = 所有请求耗时之和
urls = ['https://httpbin.org/delay/1'] * 5
start = time.time()
for url in urls:
    fetch_url(url)
print(f"串行耗时: {time.time() - start:.2f}秒")

# 多线程执行 — 总耗时 ≈ 最慢的那个请求耗时
def worker(url):
    fetch_url(url)

start = time.time()
threads = []
for url in urls:
    t = threading.Thread(target=worker, args=(url,))
    t.start()
    threads.append(t)

for t in threads:
    t.join()
print(f"多线程耗时: {time.time() - start:.2f}秒")
# 输出:多线程耗时约1秒,串行约5秒

使用concurrent.futures.ThreadPoolExecutor更简洁:

python

from concurrent.futures import ThreadPoolExecutor, as_completed

def fetch_url(url):
    response = requests.get(url)
    return url, response.status_code

urls = ['https://httpbin.org/delay/1'] * 5

with ThreadPoolExecutor(max_workers=5) as executor:
    # 提交所有任务
    futures = {executor.submit(fetch_url, url): url for url in urls}
    
    # 处理完成的结果
    for future in as_completed(futures):
        url, status = future.result()
        print(f"{url} 完成,状态码: {status}")

多线程的优势:共享内存方便、切换开销小
多线程的局限:受GIL限制,CPU密集型任务无法利用多核

二、多进程:CPU密集型任务的选择

如果你的任务是CPU密集型的,比如图像处理、大数据计算、加密解密、数学运算,那么多进程是更好的选择。

多进程会创建独立的子进程,每个进程有自己的Python解释器和内存空间,可以真正利用多核CPU并行执行。

python

import multiprocessing
import time
import math

def is_prime(n):
    """判断一个数是否是质数(CPU密集型)"""
    if n < 2:
        return False
    for i in range(2, int(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True

def find_primes(numbers):
    """从列表中找出所有质数"""
    return [n for n in numbers if is_prime(n)]

# 生成大量数字
numbers = list(range(100000, 200000))

# 串行执行
start = time.time()
result = find_primes(numbers)
print(f"串行耗时: {time.time() - start:.2f}秒")

# 多进程执行
def worker(numbers_chunk):
    return find_primes(numbers_chunk)

start = time.time()
# 获取CPU核心数
num_cores = multiprocessing.cpu_count()
chunk_size = len(numbers) // num_cores

# 分片
chunks = [numbers[i:i+chunk_size] for i in range(0, len(numbers), chunk_size)]

with multiprocessing.Pool(processes=num_cores) as pool:
    results = pool.map(worker, chunks)
    # 合并结果
    all_primes = []
    for r in results:
        all_primes.extend(r)

print(f"多进程耗时: {time.time() - start:.2f}秒")
# 多进程通常比串行快3-4倍(取决于CPU核心数)

同样可以用concurrent.futures.ProcessPoolExecutor

python

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=4) as executor:
    results = executor.map(find_primes, chunks)
    all_primes = []
    for r in results:
        all_primes.extend(r)

多进程的优势:真并行、利用多核
多进程的局限:进程创建开销大、数据共享复杂(需要序列化)、内存占用高

三、协程:高并发IO的新方案

协程是Python 3.4引入(asyncio)、3.5正式支持(async/await语法)的并发方案。它比线程更轻量,一个线程里可以运行成千上万个协程。

协程的核心是主动让出CPU:遇到IO等待时,协程会主动让出控制权,让其他协程执行。这被称为"协作式多任务",不同于线程的"抢占式多任务"。

python

import asyncio
import aiohttp
import time

async def fetch_url(session, url):
    """异步请求"""
    async with session.get(url) as response:
        return url, response.status

async def main():
    urls = ['https://httpbin.org/delay/1'] * 10
    
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        
        for url, status in results:
            print(f"{url} 完成,状态码: {status}")

start = time.time()
asyncio.run(main())
print(f"异步耗时: {time.time() - start:.2f}秒")
# 10个请求,每个延迟1秒,总耗时约1秒

协程特别适合高并发网络IO场景:Web爬虫、实时API网关、聊天服务器、游戏服务器等。

常用的异步库

  • aiohttp:异步HTTP客户端/服务器

  • aiomysqlaiopg:异步数据库驱动

  • aiofiles:异步文件操作

  • httpx:支持异步的HTTP客户端

四、实际项目中的选择策略

说了这么多,到底怎么选?我用一张表帮你决策:

场景推荐方案原因
爬虫、大量API调用协程 或 多线程IO密集型,协程更轻量
Web后端(Django/Flask)多线程(默认)框架自带,够用
Web后端(高并发)协程(FastAPI/Starlette)性能更好,资源占用少
图像处理、计算密集多进程利用多核CPU
混合任务(IO+计算)多进程+协程组合各取所长
简单定时任务单线程+协程够用且简单

五、三个真实案例

案例一:爬虫项目

需求:每天爬取10000个商品页面的价格信息。

方案:协程 + aiohttp。单机几百个并发连接,每秒爬取几百个页面,CPU和内存占用都很低。

python

# 伪代码示意
async def crawl_all():
    semaphore = asyncio.Semaphore(100)  # 限制并发数,避免被封
    
    async def crawl_one(url):
        async with semaphore:
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    return await resp.text()
    
    tasks = [crawl_one(url) for url in urls]
    pages = await asyncio.gather(*tasks)

案例二:图像批处理

需求:将一批图片缩放到指定尺寸并添加水印。

方案:多进程。图片处理是CPU密集型的,每个进程处理一批图片,充分利用多核CPU。

python

from PIL import Image
from concurrent.futures import ProcessPoolExecutor

def process_image(image_path, output_path):
    img = Image.open(image_path)
    img = img.resize((800, 600))
    # 添加水印...
    img.save(output_path)

with ProcessPoolExecutor(max_workers=8) as executor:
    futures = []
    for img_path, out_path in image_pairs:
        futures.append(executor.submit(process_image, img_path, out_path))
    
    for future in futures:
        future.result()  # 等待所有完成

案例三:实时数据流处理

需求:从Kafka消费消息,做轻量处理后再发送出去。

方案:多线程 + 协程。多线程负责消费和生产,协程处理并发。

python

import threading
import asyncio

def kafka_consumer():
    """线程:从Kafka拉取消息"""
    for message in kafka_consumer_loop():
        # 提交到协程事件循环处理
        asyncio.run_coroutine_threadsafe(
            process_message(message),
            loop
        )

async def process_message(message):
    """协程:处理单条消息(可能涉及多个外部API调用)"""
    data = await fetch_enrichment(message)
    await send_to_downstream(data)

六、常见坑和避坑指南

坑1:线程安全

多个线程共享变量时,要注意数据竞争。使用threading.Lock保护临界区。

python

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    with lock:  # 加锁
        counter += 1

坑2:多进程数据共享

多进程不共享内存,传递数据需要序列化。用multiprocessing.Queuemultiprocessing.Manager或者Redis等外部存储。

python

from multiprocessing import Manager

def worker(shared_list, value):
    shared_list.append(value)

manager = Manager()
shared_list = manager.list()

坑3:协程中不要用阻塞代码

在异步函数中调用requests.get()time.sleep()会阻塞整个事件循环。必须用异步版本的库。

python

# ❌ 错误:在协程中调用阻塞函数
async def bad_example():
    response = requests.get('http://example.com')  # 阻塞!
    time.sleep(1)  # 阻塞!

# ✅ 正确:用异步库
async def good_example():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://example.com') as resp:
            await resp.text()
    await asyncio.sleep(1)  # 异步版本

写在最后

并发编程是Python进阶路上的一道坎,但掌握了核心原理就没那么可怕。

记住几个关键点:

  • IO密集型用多线程或协程,协程更轻量但生态稍弱

  • CPU密集型用多进程,真的能利用多核

  • 协程不能和阻塞代码混用,要用异步版本的库

  • 没有银弹,根据场景选择最合适的方案

最后推荐几个进一步学习的资源:

  • 官方文档:threadingmultiprocessingasyncio模块文档

  • 《Python并发编程实战》

  • 开源项目:FastAPI的源码(大量使用异步)

多动手写实验代码,把各种方案都跑一跑,比看一百篇文章都有用。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐