Python multiprocessing 使用指南:突破 GIL 束缚的并行计算利器

Python multiprocessing 使用指南:突破 GIL 束缚的并行计算利器

作者:书到用时方恨少!

发布日期:2026年4月1日

阅读时长:约30分钟

📌 前言

Python 因其简洁易用而广受欢迎,但一个长期被诟病的缺点就是全局解释器锁(GIL) 。GIL 使得 Python 多线程无法真正利用多核 CPU 的优势。那么,如何让 Python 程序并行执行,充分发挥多核处理器的性能呢?答案就是 multiprocessing 模块。

multiprocessing 是 Python 标准库中的并行计算神器。它通过创建独立的子进程(而非线程)来绕过 GIL,让程序可以真正同时运行多个任务。同时,它提供了与 threading 模块几乎一致的 API,大大降低了学习成本。

无论你是需要加速数据处理、开发并行爬虫,还是构建高并发的系统,这篇博客都将带你从零开始,全面掌握 multiprocessing 的核心功能与最佳实践。


1. 📦 为什么需要 multiprocessing?

🔒 GIL 的局限

Python 的 GIL 是一个互斥锁,它保证同一时刻只有一个线程执行 Python 字节码。这意味着:

  • CPU 密集型任务(如数值计算、图像处理)在多线程下无法获得性能提升,甚至因为线程切换而变慢。
  • IO 密集型任务(如网络请求、文件读写)多线程仍然有效,因为线程在 IO 等待时会释放 GIL。

为了真正并行利用多核 CPU,我们需要使用多进程:每个进程拥有独立的 Python 解释器和内存空间,互不干扰,可以真正并行执行。

🚀 multiprocessing 的优势

  • 绕过 GIL:进程独立运行,无锁竞争。
  • 充分利用多核:可以并行执行 CPU 密集型任务。
  • API 友好 :与 threading 模块设计相似,易于迁移。
  • 进程间通信 :提供 QueuePipe 等多种方式。
  • 进程池:方便地管理大量进程。

2. 🧱 核心概念与基本用法

2.1 Process 类

multiprocessing.Process 用于创建和管理子进程,用法与 threading.Thread 高度相似。

python 复制代码
import multiprocessing
import time

def worker(name):
    print(f"进程 {name} 开始")
    time.sleep(2)
    print(f"进程 {name} 结束")

if __name__ == "__main__":
    p1 = multiprocessing.Process(target=worker, args=("A",))
    p2 = multiprocessing.Process(target=worker, args=("B",))

    p1.start()
    p2.start()

    p1.join()
    p2.join()
    print("所有进程结束")

关键点

  • target:进程要执行的函数。
  • args:传递给函数的参数(元组)。
  • start():启动进程。
  • join([timeout]):等待进程结束(阻塞)。
  • 必须放在 if __name__ == "__main__":,否则在 Windows 上会引发无限递归创建进程。

2.2 守护进程与终止

  • daemon:设置为守护进程,主进程结束时自动终止子进程。
  • terminate():强制终止进程(谨慎使用,可能导致资源泄漏)。
python 复制代码
p = multiprocessing.Process(target=worker, args=("D",))
p.daemon = True
p.start()
# 主进程结束后,守护进程自动结束

3. 🤝 进程间通信(IPC)

进程之间不共享内存,需要通过特定机制交换数据。

3.1 Queue

multiprocessing.Queue 提供线程/进程安全的队列,用于在进程间传递消息。

python 复制代码
import multiprocessing

def producer(queue):
    for i in range(5):
        queue.put(i)
        print(f"生产: {i}")

def consumer(queue):
    while True:
        item = queue.get()
        if item is None:  # 结束信号
            break
        print(f"消费: {item}")

if __name__ == "__main__":
    q = multiprocessing.Queue()
    p1 = multiprocessing.Process(target=producer, args=(q,))
    p2 = multiprocessing.Process(target=consumer, args=(q,))

    p1.start()
    p2.start()

    p1.join()
    q.put(None)   # 发送结束信号
    p2.join()
  • put(obj[, block[, timeout]]):放入对象(默认阻塞)。
  • get([block[, timeout]]):取出对象。
  • 队列可以存放任意可 pickle 的对象(注意:某些对象如打开的文件句柄不能跨进程传输)。

3.2 Pipe

Pipe() 返回一对连接对象 (conn1, conn2),代表管道的两端,用于双向通信。

python 复制代码
import multiprocessing

def sender(conn):
    conn.send("Hello from sender")
    conn.close()

def receiver(conn):
    msg = conn.recv()
    print(f"收到: {msg}")

if __name__ == "__main__":
    parent_conn, child_conn = multiprocessing.Pipe()
    p1 = multiprocessing.Process(target=sender, args=(child_conn,))
    p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))

    p1.start()
    p2.start()
    p1.join()
    p2.join()
  • send(obj):发送对象。
  • recv():接收对象(阻塞)。
  • 两端可以同时发送和接收,但多个进程同时读写同一端需要自己加锁。

选择建议

  • Queue 更适合一对多或多对一的生产者-消费者模式。
  • Pipe 更适合两个进程之间的简单双向通信,性能更高。

4. 🔒 同步机制

当多个进程同时访问共享资源时,需要同步原语防止数据竞争。

4.1 Lock

multiprocessing.Lockthreading.Lock 用法一致,用于保护临界区。

python 复制代码
import multiprocessing

def printer(lock, data):
    with lock:
        print(data)

if __name__ == "__main__":
    lock = multiprocessing.Lock()
    processes = []
    for i in range(10):
        p = multiprocessing.Process(target=printer, args=(lock, f"消息 {i}"))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()

4.2 Event、Semaphore 等

  • Event:用于进程间信号通知。
  • Semaphore:限制同时访问资源的进程数。
  • Condition:更复杂的条件同步。

用法与 threading 中的同名类几乎相同,不再赘述。


5. 📦 共享内存

5.1 Value 与 Array

multiprocessing.Valuemultiprocessing.Array 在共享内存中存储一个值或数组,可以被多个进程访问。

python 复制代码
import multiprocessing

def increment(val):
    with val.get_lock():
        val.value += 1

if __name__ == "__main__":
    shared_val = multiprocessing.Value('i', 0)  # 有符号整型
    processes = []
    for _ in range(10):
        p = multiprocessing.Process(target=increment, args=(shared_val,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"最终值: {shared_val.value}")   # 应该是 10
  • 第一个参数是类型码(如 'i' 表示 int,'d' 表示 double),与 array 模块相同。
  • get_lock() 返回一个锁,用于安全修改。
  • Array 用于创建共享数组:multiprocessing.Array('i', [1,2,3])

5.2 Manager

multiprocessing.Manager 提供更高级的共享对象,如列表、字典、Namespace 等,可以在不同进程间共享。它通过一个独立的服务器进程来管理对象,因此速度比 Value/Array 慢,但灵活性更高。

python 复制代码
import multiprocessing

def worker(shared_dict, key, value):
    shared_dict[key] = value

if __name__ == "__main__":
    manager = multiprocessing.Manager()
    shared_dict = manager.dict()
    processes = []
    for i in range(10):
        p = multiprocessing.Process(target=worker, args=(shared_dict, i, i*2))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(shared_dict.items())

Manager 支持的类型:

  • dict(), list(), Namespace()
  • Queue(), Value(), Array()

注意 :Manager 对象在 Windows 上创建时可能较慢,且所有操作都经过序列化,性能不如直接使用 QueuePipe


6. 🏭 进程池(Pool)

创建大量进程会消耗系统资源,进程池可以复用进程,提高效率。

6.1 基本用法

python 复制代码
import multiprocessing
import time

def square(x):
    time.sleep(0.1)   # 模拟耗时
    return x * x

if __name__ == "__main__":
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(square, range(10))
        print(results)   # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • map(func, iterable):将 iterable 中的元素分发给进程池执行,返回结果列表(保持顺序)。
  • map_async:异步版本,返回一个 AsyncResult 对象。
  • apply(func, args):同步执行单个函数。
  • apply_async:异步执行单个函数。
  • close():停止接受新任务。
  • join():等待所有任务完成(必须在 close 后调用)。

6.2 异步与回调

python 复制代码
def square(x):
    return x * x

def callback(result):
    print(f"结果: {result}")

if __name__ == "__main__":
    with multiprocessing.Pool(4) as pool:
        for i in range(10):
            pool.apply_async(square, (i,), callback=callback)
        pool.close()
        pool.join()

6.3 使用多个参数

pool.map 只支持一个可迭代参数,如果需要多个参数,可以借助 starmap

python 复制代码
def add(a, b):
    return a + b

with multiprocessing.Pool(4) as pool:
    results = pool.starmap(add, [(1,2), (3,4), (5,6)])
    print(results)  # [3, 7, 11]

7. 💡 实战案例

案例一:并行计算素数(CPU 密集型)

python 复制代码
import multiprocessing
import math

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    limit = int(math.sqrt(n)) + 1
    for i in range(3, limit, 2):
        if n % i == 0:
            return False
    return True

def find_primes_in_range(start, end):
    primes = []
    for n in range(start, end):
        if is_prime(n):
            primes.append(n)
    return primes

if __name__ == "__main__":
    total = 1000000
    chunk_size = total // 4
    ranges = [(i*chunk_size, (i+1)*chunk_size) for i in range(4)]
    ranges[-1] = (ranges[-1][0], total)  # 确保覆盖到边界

    with multiprocessing.Pool(4) as pool:
        results = pool.starmap(find_primes_in_range, ranges)
    
    primes = [p for sublist in results for p in sublist]
    print(f"找到 {len(primes)} 个素数")

案例二:多进程爬虫(IO 密集型)

虽然多进程对 IO 密集型任务不如多线程高效,但依然可以用于提升并发度(例如避免 GIL 影响)。

python 复制代码
import multiprocessing
import requests

urls = [
    "https://www.example.com",
    "https://www.python.org",
    "https://www.github.com",
    # ... 更多 URL
]

def fetch(url):
    try:
        response = requests.get(url, timeout=5)
        return url, response.status_code, len(response.content)
    except Exception as e:
        return url, None, str(e)

if __name__ == "__main__":
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(fetch, urls)
    for url, status, data in results:
        print(f"{url}: {status} - {data}")

案例三:生产者-消费者模式

使用 Queue 实现任务分发和结果收集。

python 复制代码
import multiprocessing
import time
import random

def producer(queue, task_count):
    for i in range(task_count):
        queue.put(i)
        print(f"生产任务 {i}")
        time.sleep(random.random())
    queue.put(None)  # 结束信号

def consumer(queue, results):
    while True:
        task = queue.get()
        if task is None:
            break
        result = task * task
        results.append(result)
        print(f"消费任务 {task} -> {result}")

if __name__ == "__main__":
    q = multiprocessing.Queue()
    manager = multiprocessing.Manager()
    results = manager.list()   # 共享列表

    p1 = multiprocessing.Process(target=producer, args=(q, 10))
    p2 = multiprocessing.Process(target=consumer, args=(q, results))

    p1.start()
    p2.start()
    p1.join()
    q.put(None)   # 确保 consumer 退出
    p2.join()

    print(f"所有结果: {list(results)}")

8. ⚙️ 注意事项与性能优化

8.1 进程启动方式

不同操作系统有不同启动方式:

  • spawn(Windows 默认,macOS 3.8+ 默认,Linux 可选):子进程从头开始,不继承父进程内存,安全性高,但启动较慢。
  • fork(Linux 默认,macOS 旧版本):子进程继承父进程内存,启动快,但存在死锁风险(例如在多线程环境中 fork 可能复制锁状态)。
  • forkserver(Linux 可选):通过一个服务器进程来 fork,避免死锁。

可以通过 multiprocessing.set_start_method() 设置启动方式。

python 复制代码
import multiprocessing
multiprocessing.set_start_method('spawn')  # 推荐跨平台

8.2 避免全局状态

每个进程有独立的全局变量,因此不能通过全局变量共享数据。必须使用 IPC 或共享内存。

8.3 序列化(pickle)开销

进程间传递对象需要序列化,传递大对象会显著影响性能。尽量只传递必要的数据,或使用共享内存(Value/Array)减少拷贝。

8.4 进程数设置

通常进程数设置为 CPU 核心数(multiprocessing.cpu_count()),但具体取决于任务类型:

  • CPU 密集型:通常设为核心数。
  • IO 密集型:可以设置更多,但受限于系统资源。
python 复制代码
num_workers = multiprocessing.cpu_count()

8.5 资源释放与守护进程

确保所有进程在程序退出前正确结束,避免僵尸进程。使用 join() 等待,或设置守护进程自动终止。

8.6 异常处理

子进程中的异常不会自动传递到主进程,需要在函数内部捕获并传递状态(例如通过队列返回错误信息)。

8.7 使用 with 语句管理进程池

Pool 支持上下文管理器,确保资源正确释放。

python 复制代码
with multiprocessing.Pool(4) as pool:
    results = pool.map(func, data)

9. 📊 multiprocessing vs threading vs asyncio

特性 multiprocessing threading asyncio
适用场景 CPU 密集型 IO 密集型 高并发 IO(网络)
并行性 真正并行(多核) 并发(单核) 单线程并发
内存开销 高(每个进程独立内存) 低(共享内存) 极低
进程/线程创建成本 极低
通信方式 复杂(队列、管道等) 简单(共享变量需锁) 简单(协程间直接调用)
适用操作系统 跨平台 跨平台 跨平台

选型建议

  • CPU 密集型:multiprocessing
  • IO 密集型(大量网络/文件):threadingasyncio,后者更高效。
  • 需要共享大量数据且频繁访问:threading(但要小心 GIL 影响)。

10. 🎯 总结

通过本文,我们全面学习了 Python multiprocessing 模块:

  • 为什么需要多进程:绕过 GIL,真正并行利用多核。
  • 基本用法Process 的创建、启动、等待。
  • 进程间通信QueuePipe、共享内存(Value/Array/Manager)。
  • 同步机制:锁、事件、信号量等。
  • 进程池:高效管理大量任务。
  • 实战案例:素数计算、爬虫、生产者-消费者。
  • 注意事项:启动方式、序列化开销、进程数选择。

multiprocessing 是 Python 并行编程的基石,掌握它将使你在处理大规模计算和并发任务时如虎添翼。但也要注意,多进程并非万能钥匙,合理选择并发模型才是关键。

如果你在实际项目中遇到了并行计算的有趣场景,欢迎在评论区分享!感谢阅读,我们下篇见!🚀

相关推荐
cch89182 小时前
PHP五大后台框架横向对比
开发语言·php
Warson_L2 小时前
Python 常用内置标准库
python
天真萌泪2 小时前
JS逆向自用
开发语言·javascript·ecmascript
Warson_L3 小时前
Python 函数的艺术 (Functions)
python
Warson_L3 小时前
Python 流程控制与逻辑
后端·python
野生技术架构师3 小时前
一线大厂Java面试八股文全栈通关手册(含源码级详解)
java·开发语言·面试
long_songs3 小时前
手柄键盘映射器【github链接见文末 】
python·游戏·计算机外设·pygame·软件推荐·手柄映射键盘
必然秃头3 小时前
Python 环境安装及项目构建指南
python
Warson_L3 小时前
Python 四大组合数据类型 (Collection Types)
后端·python