python 多进程

python多进程中,对于子进程的运行机制是:每个子进程中,由于不同的进程之间有独立内存,不会共享,所以每个子进程是通过分别导入所在的脚本模块来实现目标函数的运行的。对于这个机制,有以下两点需要特别注意。

1、由于每个子进程是通过导入所在脚本的模块实现模块中函数的调用的,所以,为了避免将创建子进程的语句也被导入(因为这样就会造成无限循环创建子进程,这显然是不允许的,因此python禁止了在子进程中再创建子进程,否则会报错),创建子进程的语句必须在if __name__=='__main__'语句之后定义,或者如果创建子进程的语句是定义在一个函数中的,那么这个函数调用必须在if __name__=='__main__'语句之后,这是python多进程中的强制性语法规则

2、由于子进程可直接调用的是被导入模块中的属性,因此,子进程中的目标函数应该是被导入的,这样子进程才可以调用到期需要的目标函数,因此,目标函数必须在if __name__=='__main__'语句之前定义,如果是在该语句之后定义,那么由于被导入时这部分是不会被导入的,所以运行时就会报"被导入的主模块没有目标函数属性"这样的错误。

进程池:

ProcessPoolExecutor 是 Python 标准库对多进程底层机制的高层封装,ProcessPoolExecutor 的核心目标是管理一组工作进程,将提交的任务分发下去,收集执行结果,屏蔽复杂的底层进程管理细节。其底层依赖 multiprocessing 实现进程创建、任务调度和结果回收。

concurrent.futures.ProcessPoolExecutor

from concurrent.futures import ProcessPoolExecutor, as_completed
import os
import time

# 第一步:实现单例装饰器
def singleton(cls):
    """
    单例装饰器:确保被装饰的类只能创建一个实例
    """
    _instance_cache = {}  # 缓存类的唯一实例

    def wrapper(*args, **kwargs):
        # 判断类是否已经有实例缓存
        if cls not in _instance_cache:
            _instance_cache[cls] = cls(*args, **kwargs)
        return _instance_cache[cls]
    
    return wrapper

# 第二步:封装进程池类并应用单例装饰器
@singleton
class SingletonProcessPool:
    """
    单例进程池类:封装进程池的创建、任务提交和资源释放
    """
    def __init__(self, max_workers=None):
        """
        初始化进程池
        :param max_workers: 进程池大小,默认是 CPU 核心数
        """
        # 初始化进程池(不在这里直接创建,避免提前占用资源,懒加载更优)
        self.max_workers = max_workers
        self.pool = None

    def _get_pool(self):
        """
        懒加载:获取进程池实例,不存在则创建
        """
        if self.pool is None or self.pool._shutdown:
            self.pool = ProcessPoolExecutor(max_workers=self.max_workers)
        return self.pool

    def execute_task_map(self, task_func, task_list):
        """
        批量执行任务(有序返回结果,对应 map 方法)
        :param task_func: 子进程要执行的任务函数
        :param task_list: 任务参数列表
        :return: 有序的任务结果列表
        """
        if not callable(task_func):
            raise ValueError("task_func 必须是可调用的函数")
        if not isinstance(task_list, (list, tuple)):
            raise ValueError("task_list 必须是列表或元组")
        
        pool = self._get_pool()
        # 执行任务并返回有序结果
        results = list(pool.map(task_func, task_list))
        return results

    def execute_task_submit(self, task_func, task_list):
        """
        异步提交任务(无序返回结果,对应 submit 方法,支持异常捕获)
        :param task_func: 子进程要执行的任务函数
        :param task_list: 任务参数列表
        :return: 无序的任务结果列表
        """
        if not callable(task_func):
            raise ValueError("task_func 必须是可调用的函数")
        if not isinstance(task_list, (list, tuple)):
            raise ValueError("task_list 必须是列表或元组")
        
        pool = self._get_pool()
        future_list = []
        # 批量提交任务
        for task_param in task_list:
            future = pool.submit(task_func, task_param)
            future_list.append(future)
        
        # 获取已完成的任务结果
        results = []
        for future in as_completed(future_list):
            try:
                res = future.result()
                results.append(res)
            except Exception as e:
                results.append(f"任务执行失败:{str(e)}")
        
        return results

    def shutdown(self):
        """
        手动关闭进程池,释放资源(程序退出前调用更佳)
        """
        if self.pool is not None and not self.pool._shutdown:
            self.pool.shutdown(wait=True)
            self.pool = None
            print("进程池已成功关闭,资源已释放")

# ---------------------- 测试用例 ----------------------
# 定义子进程执行的任务函数
def demo_task(num):
    """示例任务:计算平方,模拟耗时"""
    time.sleep(1)
    process_id = os.getpid()
    return f"进程ID: {process_id}, 输入: {num}, 输出: {num * num}"

if __name__ == "__main__":
    # 1. 获取单例进程池实例(多次获取也只会创建一个)
    pool_instance1 = SingletonProcessPool(max_workers=4)
    pool_instance2 = SingletonProcessPool(max_workers=8)  # 尝试创建第二个实例,会返回第一个

    # 验证是否为同一个实例
    print(f"是否为同一个实例:{pool_instance1 is pool_instance2}")  # 输出:True

    # 2. 定义任务列表
    task_list = [1, 2, 3, 4, 5, 6, 7, 8]

    # 3. 使用 map 方法执行任务(有序结果)
    print("\n=== 批量执行任务(有序结果)===")
    map_results = pool_instance1.execute_task_map(demo_task, task_list)
    for res in map_results:
        print(res)

    # 4. 使用 submit 方法执行任务(无序结果)
    print("\n=== 异步执行任务(无序结果)===")
    submit_results = pool_instance1.execute_task_submit(demo_task, task_list)
    for res in submit_results:
        print(res)

    # 5. 程序退出前关闭进程池(可选,ProcessPoolExecutor 会在程序退出时自动释放,手动关闭更规范)
    pool_instance1.shutdown()

子进程启动时「拷贝」主进程对象(最基础,自动发生)

当通过 multiprocessing.ProcessProcessPoolExecutor 创建子进程时,子进程会在启动阶段,拷贝主进程当时的内存数据(包括已创建的对象),从而获取到主进程对象的 “副本”。

1. 表现与特点

  • 子进程获取到的是「对象的副本」,不是原对象:子进程对副本的修改,不会影响主进程中的原对象,也不会影响其他子进程的副本(因为内存空间独立)。
  • 拷贝仅发生在「子进程启动瞬间」:子进程启动后,主进程再修改原对象,子进程的副本不会同步更新。
  • 支持大部分简单对象(整数、字符串、列表、字典等),但对象必须可被「序列化 / 拷贝」(少数特殊对象如文件句柄、网络连接,拷贝后可能失效)。

主动「跨进程传递 / 共享」主进程对象(按需实现,支持同步)

如果需要让子进程获取主进程对象的「最新状态」,或让多个子进程共享同一个对象(修改互相可见),就需要通过 multiprocessing 提供的 IPC 组件 主动实现,这也是我们之前提到的核心组件的用途。

1. 方式 1:通过进程参数传递对象(一次性传递)

创建子进程时,将主进程对象作为参数传递给子进程的任务函数,本质是「序列化 + 传输 + 反序列化」,而非直接共享。

特点

  • 传递的是对象的「快照」(传递瞬间的状态),子进程获取到的是反序列化后的新对象,修改不会影响主进程原对象。
  • 要求对象必须可被 pickle 序列化(大部分 Python 内置对象都支持,自定义对象默认也支持,除非包含不可序列化的属性如文件句柄)。

2.通过共享组件实现「实时共享」(支持同步修改)

如果需要子进程和主进程、子进程之间共享同一个对象(修改互相可见),需要使用 multiprocessing 提供的 ValueArrayManager 等共享组件,这些组件会在多个进程间创建「共享内存区域」或「中间管理进程」,实现对象的实时同步。

特点

  • 子进程获取到的是「共享对象的引用」,修改会直接作用于共享区域,主进程和其他子进程都能看到最新状态。
  • 自带进程安全保障(部分需要手动加锁),避免数据混乱。
  • 支持的对象类型有限(Value/Array 支持简单类型,Manager 支持复杂类型)。

特殊情况:无法被子进程获取 / 使用的对象

有一些特殊对象,既无法被拷贝,也无法被序列化传递,子进程无法正常获取和使用,常见的有:
  1. 文件句柄、网络套接字(socket)、数据库连接:这些对象与操作系统的资源绑定,拷贝或序列化后会失效,子进程无法复用。
  2. 线程锁、进程锁(未通过共享组件传递的):锁是与特定进程 / 线程绑定的,拷贝后无法实现同步功能。
  3. 生成器、迭代器:无法被 pickle 序列化,无法传递给子进程。
  4. 部分内置特殊对象(如 sys.stdinsys.stdout):与当前进程的 IO 流绑定,子进程无法正常使用。

 

posted @ 2023-06-12 14:28  wangssd  阅读(95)  评论(0)    收藏  举报