Python并发编程学习 day4
2025-07-06 22:20 第二个卿老师 阅读(36) 评论(0) 收藏 举报Day 4:Python 多进程编程进阶
一、 基础使用
多进程适用于处理那些需要CPU进行大量计算的任务,和多线程类似,目前标准库中有基础的multiprocessing模块与高级的concurrent.futures模块,任务需要复杂控制用multiprocessing,简单任务用concurrent.futures
multiprocessing.Process模块- 用法:通过multiprocessing.Process创建并启动线程
- 关键参数:
target/name/args/kwargs等 - 常用方法:
start()/join()/is_alive()/等 - 示例:
p = multiprocessing.Process(target=worker, args=('worker1',)) p.start() p.join() # 这会阻塞调用这个方法的进程,直到被调用 join() 的j进程终结
为了新的 Python 解释器可以安全地导入主模块,Process必须在if__name__=='main' 块中创建
multiprocessing.Pool模块- 用法:偏底层的的进程池创建,不支持任务取消,支持python2版本
- 关键参数:
processes/maxtasksperchild等 - 常用方法:
apply()/apply_async()/map()/imap_unordered()等 - 示例:
with Pool(processes=4) as pool: results = pool.map(f, data_list)
Pool支持惰性迭代结果,适用于流式处理大型数据集时、高级共享内存等场景
concurrent.futures模块- 用法:高层次、更现代的进程池创建接口,通过ProcessPoolExecutor对象来异步执行
- 关键参数:
max_workers/max_tasks_per_child等 - 常用方法:
submit()/map()等 - 示例:
with ProcessPoolExecutor(max_workers=4) as executor: future = executor.submit(worker, "task1")
默认工作进程数与pool一样(os.process_cpu_count()),ProcessPoolExecutor支持复杂任务生命周期管理,特别是涉及任务取消的场景,python3版本推荐使用
二、进程间通信(IPC)
多进程之间通信与多线程不同,进程拥有独立的地址空间和资源,无法直接访问其他进程的内存。因此,IPC必须依赖操作系统提供的机制(如管道、消息队列、共享内存等)实现数据交换
Queue(队列)- 类似queue.Queue,使用一个管道和少量锁和信号量实现的共享队列实例
- 方法:
get()/put()等 - 示例:
def producer(q): for i in range(5): q.put(i) def consumer(q): while True: item = q.get() if item is None: break if __name__ == '__main__': q = 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()
队列是线程和进程安全的,任何放入 multiprocessing 队列的对象都将被序列化
Pipe(管道)- 返回一个由管道连接的连接对象,默认情况下是双工(双向)的,适用于两个进程间的通信
- 方法:
send()/recv() - 示例:
parent_conn, child_conn = Pipe() # 可设置单向通道(duplex=False) def child_process(conn): conn.send("Hello from child") p = multiprocessing.Process(target=child_process, args=(child_conn,)) p.start() parent_conn.send("Hello from parent") p.join()
管道中send() 方法将使用 pickle 来序列化对象,而 recv() 将重新创建对象,如果两个进程(或线程)同时尝试读取或写入管道的同一端,则管道中的数据可能会损坏
Shared memory(共享内存)- 通过系统调用分配一块多个进程可访问的内存共享区域,包括Value,Array两种
- 示例:
# 共享Value counter = multiprocessing.Value('i', 0) # 共享Array arr = multiprocessing.Array('d', [0.0, 1.0, 2.0])
共享内存是进程和线程安全的,追求更灵活的方式,也可以使用multiprocessing.sharedctypes模块,另外需要共享大型数据块,可以使用multiprocessing.shared_memory模块(Python 3.8+)
Server process(服务进程)- 通过控制服务进程来保存Python对象并允许其他进程使用代理操作
- 方法:
acquire()/release() - 示例:
with Manager() as m: shared_list = m.list() shared_dict = m.dict()
适用与在进程间共享复杂数据结构(字典、列表等)场景,也支持跨网络的其他主机访问
三、进程同步原语
多进程的同步原语和多线程类似,但是在同步原语机制中,多线程是用户态锁(轻量级),切换代价小,所以性能快,而多进程是操作系统内核态同步(系统调用),切换代价大,所以性能慢,但可以利用多核
Lock(互斥锁)- 用法:保护临界区,避免竞态,推荐使用上下文管理器来运行
- 方法:
acquire()/release() - 示例:
lock = multiprocessing.Lock() with lock: ...
与多线程类似,acquire()可以阻塞或非阻塞获取锁,用release()释放锁,在同一个进程中,某个线程获取了进程锁,虽然其他线程释放锁不报错,但只有该线程才能真正释放锁
RLock(可重入锁)- 与普通锁区别:同一进程或线程可多次
acquire(),避免死锁,有稍微的性能开销 - 示例:
lock = multiprocessing.RLock() with lock: ...- 与普通锁区别:同一进程或线程可多次
Event(事件)- 用法:进程间事件通知
- 方法:
set()/clear()/wait() - 示例:
event = multiprocessing.Event() def waiter(): print("Waiting for event") event.wait() print("Event received") def setter(): time.sleep(2) event.set()
底层机制中,threading.Event是python对象,multiprocessing.Event是操作系统同步对象(信号)
-
Condition(条件变量)- 用法:更复杂的线程同步,配合
Lock使用 - 方法:
wait()/notify()/notify_all() - 示例:
def serve_customers(customer_available_condition, customer_queue): while True: with customer_available_condition: # 等待顾客到来 while not customer_queue: customer_available_condition.wait() # 服务顾客 customer = customer_queue.pop(0) def add_customer_to_queue(name, customer_available_condition, customer_queue): """添加顾客的进程函数""" with customer_available_condition: customer_queue.append(name) customer_available_condition.notify() # 通知服务进程 if __name__ == '__main__': # 使用Manager创建共享对象 with Manager() as manager: customer_available_condition = Condition() customer_queue = manager.list() # 共享列表 # 启动服务进程 server = Process(target=serve_customers, args=(customer_available_condition, customer_queue)) server.start() customer_names = [ "Customer 1", "Customer 2" ] adders = [] for name in customer_names: p = Process(target=add_customer_to_queue,args=(name, customer_available_condition, customer_queue)) p.start() adders.append(p) # 等待所有添加进程完成 for p in adders: p.join() - 用法:更复杂的线程同步,配合
-
Semaphore(信号量)- 用法:使用计数器来限制多个进程对临界区的访问
- 关键参数:
value - 方法:
acquire()/release() - 示例:
def serve_customer(name, sem): with sem: time.sleep(2) # 模拟服务时间 if __name__ == '__main__': teller_semaphore = Semaphore(2) # 创建跨进程信号量 customers = [ "Customer 1", "Customer 2" ] processes = [] for customer_name in customers: p = Process(target=serve_customer, args=(customer_name, teller_semaphore)) p.start() processes.append(p) for p in processes: p.join() -
Barrier(栅栏)- 用法:允许一组进程相互等待后再继续执行,类似于性能测试的集合点
- 关键参数:
value/action/timeout - 方法:
wait()/reset() - 示例:
def prepare_for_work(name, barrier): barrier.wait() # 等待所有进程到达屏障 if __name__ == '__main__': teller_barrier = Barrier(3) # 创建3个进程的屏障 tellers = ["Teller 1", "Teller 2", "Teller 3"] processes = [] for teller_name in tellers: p = Process(target=prepare_for_work, args=(teller_name, teller_barrier)) p.start() processes.append(p) for p in processes: p.join()
四、多进程实战:并行计算
import math
from concurrent.futures import ProcessPoolExecutor
def is_prime(n):
if n < 2:
return False
for i in range(2, int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
if __name__ == '__main__':
numbers = range(1000000, 1001000)
with ProcessPoolExecutor() as executor:
results = list(executor.map(is_prime, numbers))
primes = [n for n, prime in zip(numbers, results) if prime]
print(f"Found {len(primes)} primes")
🔁 今日思考题
- 为什么进程间通信比线程间通信成本更高?如何优化?
答:进程间通信成本高原因:
1,由于每个进程有自己独立的内存空间,共享数据时存在耗时的复制操作。
2,进程间通信通过IPC机制时,由于传递复杂数据结构对象,数据需要额外的序列化与反序列化。
3,进程间通信需要操作系统内核处理,上下文切换开销大。
4,同步原语实现机制更复杂,开销较大。
如何优化:
1,减少数据传输量,只传递必要的数据或采用共享内存方式等
2,选择高效的IPC机制,如共享内存 (shared_memory模块)
3,批量发送数据,减少序列化次数
4,优化程序设计,避免频繁通信
5,使用其他高效的序列化库 - 在Python多进程编程中如何避免产生僵尸进程?
答:在Python多进程编程中,当子进程结束了执行,但它的父进程还没有调用 wait() 或 waitpid() 来获取它的退出状态时,子进程就会变成僵尸进程。
如何避免:
1,使用 process.join(),join() 会阻塞父进程,直到子进程终止,join()内部会自动处理子进程的退出状态
2,使用 multiprocessing.Pool,Pool 对象内部会自动处理进程的启动
3,设置子进程为守护进程 (Daemon Process) - 频繁创建销毁进程池有什么性能问题?如何优化?
答:会带来3个问题:
1,进程创建成本高,启动慢。
2,上下文切换频繁。
3,操作系统资源浪费。
如何优化:
1,进程池复用,可创建全局进程池并重复使用
2,使用队列(Queue)进行任务分发
3,合理设置进程池大小
浙公网安备 33010602011771号