代码改变世界

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. 为什么进程间通信比线程间通信成本更高?如何优化?
    答:进程间通信成本高原因:
    1,由于每个进程有自己独立的内存空间,共享数据时存在耗时的复制操作。
    2,进程间通信通过IPC机制时,由于传递复杂数据结构对象,数据需要额外的序列化与反序列化。
    3,进程间通信需要操作系统内核处理,上下文切换开销大。
    4,同步原语实现机制更复杂,开销较大。
    如何优化:
    1,减少数据传输量,只传递必要的数据或采用共享内存方式等
    2,选择高效的IPC机制,如共享内存 (shared_memory模块)
    3,批量发送数据,减少序列化次数
    4,优化程序设计,避免频繁通信
    5,使用其他高效的序列化库
  2. 在Python多进程编程中如何避免产生僵尸进程?
    答:在Python多进程编程中,当子进程结束了执行,但它的父进程还没有调用 wait() 或 waitpid() 来获取它的退出状态时,子进程就会变成僵尸进程。
    如何避免:
    1,使用 process.join(),join() 会阻塞父进程,直到子进程终止,join()内部会自动处理子进程的退出状态
    2,使用 multiprocessing.Pool,Pool 对象内部会自动处理进程的启动
    3,设置子进程为守护进程 (Daemon Process)
  3. 频繁创建销毁进程池有什么性能问题?如何优化?
    答:会带来3个问题:
    1,进程创建成本高,启动慢。
    2,上下文切换频繁。
    3,操作系统资源浪费。
    如何优化:
    1,进程池复用,可创建全局进程池并重复使用
    2,使用队列(Queue)进行任务分发
    3,合理设置进程池大小