Python多进程多锁为何比单锁运行更慢?求技术解析
我正在做Python multiprocessing实验,代码需要并发修改3个跨进程共享变量(dict、float、int)。按锁机制的逻辑,给每个变量分配单独的锁应该更高效,能避免进程因为修改无关变量而等待。但实际测试下来,单锁版本耗时2.1秒,多锁版本反而耗时2.85秒,比单锁还慢。
单锁版本代码
import multiprocessing as mp import numpy as np import time class ToyClass: def __init__(self, shared_a, shared_b): self.a = shared_a self.b = shared_b def update_a(self, key, n, lock): with lock: if key not in self.a: self.a[key] = np.zeros(4) self.a[key][n] += 1 def update_b(self, lock): with lock: self.b.value = max(0.1, self.b.value - 0.01) def run_episode(toy, counter, lock): key = np.random.randint(100) n = np.random.randint(4) toy.update_a(key, n, lock) toy.update_b(lock) with lock: counter.value += 1 if __name__ == "__main__": num_episodes = 1000 num_processes = 4 t0 = time.time() with mp.Manager() as manager: shared_a = manager.dict() shared_b = manager.Value('d', 0) counter = manager.Value('i', 0) toy = ToyClass(shared_a=shared_a, shared_b=shared_b) # Single lock lock = manager.Lock() pool = mp.Pool(processes=num_processes) for _ in range(num_episodes): pool.apply_async(run_episode, args=(toy, counter, lock)) pool.close() pool.join() tf = time.time() print(f"Time to compute single lock: {tf - t0} seconds")
多锁版本代码
import multiprocessing as mp import numpy as np import time class ToyClass: ## Same definition as for single lock def __init__(self, shared_a, shared_b): self.a = shared_a self.b = shared_b def update_a(self, key, n, lock): with lock: if key not in self.a: self.a[key] = np.zeros(4) self.a[key][n] += 1 def update_b(self, lock): with lock: self.b.value = max(0.1, self.b.value - 0.01) def run_episode(toy, counter, lock_a, lock_b, lock_count): key = np.random.randint(100) n = np.random.randint(4) toy.update_a(key, n, lock_a) toy.update_b(lock_b) with lock_count: counter.value += 1 if __name__ == "__main__": num_episodes = 1000 num_processes = 4 t0 = time.time() with mp.Manager() as manager: shared_a = manager.dict() shared_b = manager.Value('d', 0) counter = manager.Value('i', 0) toy = ToyClass(shared_a=shared_a, shared_b=shared_b) # 3 locks for 3 shared variables lock_a = manager.Lock() lock_b = manager.Lock() lock_count = manager.Lock() pool = mp.Pool(processes=num_processes) for _ in range(num_episodes): pool.apply_async(run_episode, args=(toy, counter, lock_a, lock_b, lock_count)) pool.close() pool.join() tf = time.time() print(f"Time to compute multi-lock: {tf - t0} seconds")
疑问
请问我忽略了什么?是不是锁切换的计算开销超过了潜在收益?锁不就是个标记吗,为什么会出现这种情况?
注:我知道单进程/线程运行更快,这个实验就是为了理解多进程的缺点才做的。
核心原因:跨进程锁的额外开销 + 实验场景不匹配
Manager.Lock是跨进程远程锁,IPC开销大
你用的mp.Manager()创建的锁不是本地线程锁,而是依赖Manager服务进程中转的跨进程锁。每次获取/释放锁都要进行进程间通信(IPC),这种通信本身就有不小的开销。单锁版本只需要3次IPC操作(分别对应update_a、update_b、counter的锁操作),但多锁版本需要3次不同锁的独立IPC,累积下来的开销直接超过了多锁带来的并发收益。当前场景锁竞争极低,多锁优势无法体现
你的每个run_episode里,锁的持有时间极短:只是简单的字典键检查、数组元素自增、浮点值更新、计数器加1。这种情况下,单锁的等待时间几乎可以忽略不计,反而多出来的几次锁IPC操作的开销成了主要耗时点。只有当锁持有时间很长(比如每个锁内执行1秒的计算任务)时,多锁才能让不同变量的修改并行,这时多锁的优势才会显现。多锁带来更多内核态/用户态切换开销
虽然锁本质是"标记",但跨进程锁的获取/释放会触发内核级的调度动作。多锁意味着更多次的内核态与用户态之间的切换,这些切换的开销远大于你想象。单锁版本中,进程拿到一次锁后可以连续完成三个操作,减少了切换次数;而多锁版本需要三次申请不同的锁,每次都可能触发调度,累积起来就导致整体变慢。
验证建议
- 若想测试多锁的优势,可在
update_a的锁内添加耗时任务(比如time.sleep(0.1)),此时单锁会让所有进程排队等待,多锁则能让不同变量的修改并行,多锁版本的耗时会显著低于单锁。 - 如果使用本地锁(
mp.Lock())而非Manager.Lock(),开销会小很多,但注意Manager创建的共享变量必须配合Manager的锁使用,否则会出现同步问题。
内容的提问来源于stack exchange,提问作者Leo

