3. 多核编程模型:共享内存模型 vs 消息传递模型,数据并行与任务并行
聊到多核编程,绕不开两个核心模型:共享内存和消息传递。说白了,它们就是多核之间「怎么聊天、怎么干活」的两种基本方式。
我个人习惯把共享内存模型想象成「合租一套房」——大家共用客厅、厨房,谁都能拿冰箱里的东西。而消息传递模型更像是「各自住单间」,想借个酱油得敲门递纸条。
嗯,咱们先看看这两种模型到底怎么玩。
3.1 共享内存模型
共享内存模型,就是多个线程或进程直接读写同一块内存区域。听起来简单,对吧?但坑也在这里。
我在项目中遇到过一个问题:两个线程同时更新一个计数器,结果跑出来的值永远比预期少。为什么?因为 count++ 不是原子操作——它分三步:读、改、写。两个线程同时读,都读到 100,各自加 1 再写回去,结果还是 101,而不是 102。
Python 里用 threading 模块就能体验共享内存模型。看个例子:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # 加锁保护共享资源
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 输出 400000,正确!
你想想看,如果不加锁,结果大概率不是 400000。这就是共享内存模型的典型场景——数据共享方便,但同步开销大。
queue.Queue 内部已经做了线程安全,优先用它。
3.2 消息传递模型
消息传递模型,每个进程/线程有自己的私有内存,不共享数据。它们通过发送和接收消息来通信。
我曾经调试过一个分布式爬虫系统,用的就是消息传递模型。每个爬虫节点独立运行,通过消息队列交换任务和结果。好处很明显——一个节点挂了不影响其他节点,坏处是……消息格式设计不好,解析起来能让人崩溃。
Python 的 multiprocessing 模块支持消息传递:
from multiprocessing import Process, Queue
def worker(q):
data = q.get() # 接收消息
result = data * 2
q.put(result) # 发送结果
if __name__ == '__main__':
q = Queue()
q.put(42)
p = Process(target=worker, args=(q,))
p.start()
p.join()
print(q.get()) # 输出 84
消息传递模型天然避免了数据竞争,因为根本没有共享数据。但代价是——每次通信都要序列化/反序列化数据,性能开销不小。
3.3 数据并行 vs 任务并行
搞清楚了通信模型,咱们再看看并行计算的两种组织方式。
数据并行
把大数据拆成小块,每个核心处理一块,最后合并结果。说白了就是「人多力量大,一起搬砖」。
举个例子,对 100 万个数字求和:
from multiprocessing import Pool
def sum_chunk(chunk):
return sum(chunk)
data = list(range(1_000_000))
chunks = [data[i:i+250_000] for i in range(0, 1_000_000, 250_000)]
with Pool(4) as p:
results = p.map(sum_chunk, chunks)
total = sum(results)
print(total) # 499999500000
数据并行最适合矩阵运算、图像处理、批量数据转换这类场景。我在做金融风控系统时,每天要处理上亿条交易记录,就是用数据并行把任务分到 32 个核上跑,时间从 40 分钟压到了 2 分钟以内。
任务并行
任务并行是把不同的任务分配给不同的核心。每个任务做的事情不一样,数据也可能不一样。
比如一个系统同时做三件事:下载数据、解析数据、写入数据库。这三个任务可以并行执行:
import threading
import time
def download():
time.sleep(2)
print("下载完成")
def parse():
time.sleep(1)
print("解析完成")
def write():
time.sleep(3)
print("写入完成")
threads = [
threading.Thread(target=download),
threading.Thread(target=parse),
threading.Thread(target=write)
]
for t in threads: t.start()
for t in threads: t.join()
# 总耗时约 3 秒,而不是 6 秒
任务并行适合流水线作业、事件驱动系统、Web 服务器等场景。但要注意——任务之间如果有依赖关系,并行度会大打折扣。
- 数据量大、操作相同 → 数据并行
- 任务不同、相互独立 → 任务并行
- 需要频繁通信 → 共享内存模型
- 需要高容错、跨机器 → 消息传递模型
3.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。我建议你多看几遍,把模型和并行方式对应起来:
3.5 避坑指南
最后分享几个我踩过的坑:
- 不要迷信共享内存——我曾经为了省去消息序列化的开销,强行用共享内存做跨机器通信,结果网络延迟和缓存一致性协议把我坑惨了。跨机器老老实实用消息传递。
- 数据并行不是万能药——如果每个数据块的处理时间差异很大(比如有的块要 1 秒,有的要 10 秒),数据并行的效率会很低。这时候任务并行+动态调度更合适。
- 锁的粒度要适中——锁太大,并行度低;锁太小,死锁风险高。我一般先粗粒度实现,性能瓶颈时再用细粒度锁优化。
可可精品资料库