笔者上个月处理200万条用户行为日志,单线程清洗要13秒。心想8核服务器用多进程应该飞快,于是写了如下代码:
- import multiprocessing as mp
- CONFIG = {"date_format": "%Y-%m-%d", "max_len": 100}
- results = []
- def process_line(line):
- cleaned = do_clean(line, CONFIG)
- results.append(cleaned)
- return cleaned
- if __name__ == "__main__":
- with mp.Pool(4) as pool:
- pool.map(process_line, data)
- print(len(results)) # 输出0!
复制代码
运行后主进程的results依然是空列表。原因很简单:每个子进程拥有独立的内存空间,主进程定义的results在子进程中是拷贝,修改不影响主进程。Python多进程间通信必须显式传递数据,下面介绍三种正确姿势。
---
## 一、Queue:传送带式通信
multiprocessing.Queue内部处理了同步问题,多个进程可以安全地put和get。适用于生产者-消费者模式、任务分发和结果收集。
### 经典用法
- from multiprocessing import Process, Queue
- def worker(q):
- while True:
- item = q.get()
- if item is None: # 结束信号
- break
- result = item * 2
- q.put(result)
- if __name__ == "__main__":
- q = Queue()
- p = Process(target=worker, args=(q,))
- p.start()
- for i in range(10):
- q.put(i)
- q.put(None) # 通知子进程结束
- p.join()
- while not q.empty():
- print(q.get())
复制代码
关键点:put()和get()默认阻塞,队列满时put等待,队列空时get等待。常用None作为结束信号。适合任务数量不确定、动态分发的场景。性能中等,需要序列化。
---
## 二、Manager:共享复杂数据结构
Manager启动一个独立的管理器进程,通过代理对象实现共享。操作共享列表、字典就像操作普通对象,但每次读写都有进程间通信开销。
### 示例
- from multiprocessing import Process, Manager
- def worker(shared_list, shared_dict, value):
- shared_list.append(value)
- shared_dict[value] = value * 2
- if __name__ == "__main__":
- with Manager() as manager:
- shared_list = manager.list(["初始数据"])
- shared_dict = manager.dict()
- processes = []
- for i in range(5):
- p = Process(target=worker, args=(shared_list, shared_dict, i))
- processes.append(p)
- p.start()
- for p in processes:
- p.join()
- print(list(shared_list)) # ['初始数据', 0, 1, 2, 3, 4]
- print(dict(shared_dict)) # {0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
复制代码
支持list、dict、Namespace、Lock、Queue等。数据量不大、需要共享复杂容器时使用。注意:每次操作都有IPC开销,性能比直接内存访问慢。
---
## 三、SharedMemory:零拷贝内存共享
SharedMemory让多个进程直接读写同一块物理内存,无需序列化,速度最快。Python 3.8+内置。适合大数据量、高性能计算,尤其是NumPy数组共享。
### 基础用法
- from multiprocessing import shared_memory
- import numpy as np
- # 主进程创建共享内存
- shm = shared_memory.SharedMemory(create=True, size=1024)
- arr = np.ndarray((256,), dtype=np.int32, buffer=shm.buf)
- arr[:] = np.arange(256)
- # 子进程通过shm.name连接同一块内存
- # shm_b = shared_memory.SharedMemory(name=shm.name)
- # arr_b = np.ndarray((256,), dtype=np.int32, buffer=shm_b.buf)
- # 修改arr_b即修改arr
- shm.close()
- shm.unlink() # 必须释放
复制代码
### 使用SharedMemoryManager自动管理生命周期
- from multiprocessing.managers import SharedMemoryManager
- with SharedMemoryManager() as smm:
- sl = smm.ShareableList(range(2000))
- # 多个进程共享sl
- # 退出with自动释放所有共享内存
复制代码
性能极高(零拷贝),但需要开发者管理内存分配和释放。适合MB级以上的数据频繁读写。
---
## 决策原则
| 通信方式 | 适用场景 | 性能 | 易用性 |
|---------|---------|------|-------|
| Queue | 任务分发、结果收集 | 中等(需序列化) | ★★★★★ |
| Manager | 共享列表/字典等复杂结构 | 较慢(协调开销) | ★★★★ |
| SharedMemory | 大数组、高性能需求 | 极快(零拷贝) | ★★★(需手动管理) |
一句话选择:只传递任务和结果选Queue;需要共享容器、数据量不大选Manager;共享大数组、追求极致性能选SharedMemory。
回到开头的数据处理任务,最终改用Queue方案:主进程将200万条日志分批放入Queue,4个子进程轮流取数据处理,结果放入另一个Queue,主进程收集。耗时从13秒降到3.6秒,再也不会出现results为0的尴尬。
多进程内存隔离是Python并发编程的常见陷阱。掌握Queue、Manager、SharedMemory三种通信方式,就能让数据在进程间安全、高效地流通。 |