跳转至

第 11 章 · 并发模型

并发是你作为资深 Java 工程师的强项——你熟悉线程、锁、ExecutorServiceCompletableFuture。但 Python 有一个让所有 Java 程序员翻车的东西:GIL(全局解释器锁)。本章讲透 GIL,对照 threading/multiprocessing/concurrent.futures,并给 asyncio 一个概览。


11.1 GIL:为什么 Python 多线程"不能"并行 ⚠️

GIL(Global Interpreter Lock) 是 CPython 实现的一个机制:同一时刻,只有一个线程能执行 Python 字节码

Java:          4 个线程 → 4 个 CPU 核心真正并行跑
Python(GIL):   4 个线程 → 抢一把锁,同一刻只有一个在跑 Python 代码

后果

  • CPU 密集型任务,多线程无法利用多核——甚至比单线程更慢(多了锁竞争和切换开销)。
  • IO 密集型任务(网络、磁盘),多线程仍然有效——因为线程在等 IO 时会释放 GIL,其他线程能跑。

⚠️ Java 程序员的陷阱(最致命的一条)

别拿 Java 直觉写 Python 的 CPU 密集多线程,以为能加速。一段纯计算的多线程代码在 Python 里不会变快。要 CPU 并行,用 multiprocessing(见 11.3)。

为什么有 GIL:CPython 用引用计数管理内存,GIL 简化了线程安全的实现。它是实现细节,不是语言规范——其他实现(Jython、IronPython)没有 GIL;CPython 3.13 起也有实验性的 free-threading(no-GIL)构建,但 2026 年默认仍带 GIL。

场景 GIL 影响 推荐
CPU 密集(数值计算) ❌ 多线程无益 multiprocessing 或 C 扩展/NumPy
IO 密集(网络/文件) ✅ 等待时释放 GIL threading / asyncio
混合 视情况 concurrent.futures

11.2 threadingjava.lang.Thread

  • Java

    Thread t = new Thread(() -> work());
    t.start(); t.join();
    
    synchronized (lock) { shared++; }
    
  • Python

    import threading
    
    t = threading.Thread(target=work)
    t.start(); t.join()
    
    lock = threading.Lock()
    with lock:
        shared += 1
    

注意:

  • threading.Thread 接收 target(函数)和 args——因为函数是一等公民(第 2 章)。
  • 锁用 with lock:(上下文管理器),比 Java 的 try/finally 释放更优雅,且保证异常时释放。
  • 即便有锁,CPU 密集仍受 GIL 限制——锁保护的是数据一致性,不解决并行性。

11.3 multiprocessing:绕过 GIL 实现真并行

要 CPU 多核并行,用多进程——每个进程有独立的解释器和 GIL,真正并行:

  • Java(多线程即可并行)

    ExecutorService pool =
      Executors.newFixedThreadPool(4);
    
  • Python(多进程才并行)

    from multiprocessing import Pool
    
    with Pool(4) as pool:
        results = pool.map(heavy_work, items)
    

代价:进程比线程(独立内存空间),进程间通信(队列、管道)要序列化,开销大。所以 multiprocessing 适合"任务大、计算重、少通信"的场景。

Java 程序员的视角

在 Java 你用多线程做 CPU 并行;在 Python 你得用多进程。这是最大的思维切换。NumPy/C 扩展能在内部释放 GIL 实现真正的多线程并行,所以数值计算走 NumPy 而非手写多进程。


11.4 concurrent.futures:高层抽象 ↔ ExecutorService

最常用的"提交任务、拿结果"抽象,对应 Java 的 ExecutorService

  • Java

    var pool = Executors.newFixedThreadPool(10);
    List<Future<String>> fs = urls.stream()
        .map(u -> pool.submit(() -> fetch(u)))
        .toList();
    
  • Python

    from concurrent.futures import ThreadPoolExecutor, as_completed
    
    with ThreadPoolExecutor(max_workers=10) as pool:
        futures = [pool.submit(fetch, u) for u in urls]
        for f in as_completed(futures):
            print(f.result())
    
  • ThreadPoolExecutor:IO 密集(受 GIL,但等待时让出)。
  • ProcessPoolExecutor:CPU 密集(真并行,内部用 multiprocessing)。
  • pool.map(func, items):批量提交 + 按序收集,最省心。

选择建议

不确定就先用 concurrent.futures——它屏蔽了线程/进程的细节,API 统一,是"日常并行"的默认选择。


11.5 asyncio 概览:单线程高并发 ↔ 响应式 / 虚拟线程

asyncio 是 Python 的协程并发模型——单线程内用事件循环切换任务,实现极高并发(上万连接),没有线程切换开销。

import asyncio

async def fetch(url: str) -> str:
    await asyncio.sleep(1)        # await 让出控制权,事件循环去跑别的任务
    return f"done {url}"

async def main():
    # 并发抓取所有 url(总耗时约 1 秒,而非 N 秒)
    results = await asyncio.gather(*(fetch(u) for u in urls))

asyncio.run(main())               # 启动事件循环

三个关键概念

概念 含义 Java 对应
async def 定义协程(不能直接调用,要 await CompletableFuture
await 等待一个协程完成(期间让出) .thenApply/.join
asyncio.gather 并发执行多个协程 CompletableFuture.allOf

何时用 asyncio:IO 密集 + 高并发(爬虫、Web 服务器、数据库连接池)。FastAPI/aiohttp 等现代框架都基于它。

何时不用:CPU 密集(GIL + 单线程不帮你)、简单脚本(asyncio 的心智负担不值得)。

和 Java 的对应

  • asyncio ≈ Java 的响应式(Reactor/RxJava)或 虚拟线程(Virtual Threads, Java 21):都是为高并发 IO 设计。
  • 区别:asyncio 是显式 async/await 传染(一旦 async,调用链都得 async);Java 虚拟线程让你用同步代码写高并发,心智更轻。PEP 703 的 free-threading(no-GIL)已在 3.13 提供实验性构建,未来或成默认。

11.6 选择指南

你的任务是什么?
├─ CPU 密集(数值/计算)
│   └─ multiprocessing / ProcessPoolExecutor / NumPy
├─ IO 密集,并发量小(几个~几十)
│   └─ threading / ThreadPoolExecutor
└─ IO 密集,并发量大(成百上千连接)
    └─ asyncio(配合 aiohttp/FastAPI)

一句话总结

Java 里"多线程"是万能解;Python 里你得先分清 CPU 还是 IO:CPU 走多进程,IO 小并发走多线程,IO 大并发走 asyncio。


本章练习

练习 11.1

解释为什么下面这段"多线程加速计算"在 Python 里往往比单线程更慢:

from threading import Thread
def heavy(n): s = sum(i*i for i in range(n))
threads = [Thread(target=heavy, args=(10**7,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()

参考答案

heavy 是纯 CPU 计算,受 GIL 限制——四个线程同一时刻只有一个在真正执行 Python 字节码,无法并行。加上线程切换和锁竞争的开销,总时间反而比单线程顺序跑四次更长。要加速应改用 multiprocessing.Pool,让每个进程独立 GIL 真正并行。

练习 11.2

concurrent.futures.ThreadPoolExecutor 并发请求一批 URL(用 urllib.request.urlopen),收集状态码。

参考答案

from concurrent.futures import ThreadPoolExecutor
import urllib.request

def status(url):
    try:
        with urllib.request.urlopen(url, timeout=5) as r:
            return url, r.status
    except Exception as e:
        return url, str(e)

urls = ["https://example.com", "https://python.org"]
with ThreadPoolExecutor(max_workers=8) as pool:
    for url, code in pool.map(status, urls):
        print(url, code)
要点:IO 密集,线程在等待网络时释放 GIL,多线程有效。

练习 11.3

asyncio 改写:并发 sleep 多个不同时长,总耗时约等于最长那个(而非总和)。

参考答案
import asyncio

async def task(i, seconds):
    await asyncio.sleep(seconds)
    return f"task {i} done after {seconds}s"

async def main():
    results = await asyncio.gather(
        task(1, 1), task(2, 2), task(3, 1.5)
    )
    print(results)

asyncio.run(main())   # 总耗时约 2 秒

上一章:第 10 章 · 工程化基础 | 下一章:第 12 章 · 常见陷阱与反 Java 习惯