在 Python 中,动态加载模块通常是通过使用 importlib 库实现的,而处理多进程问题,则可利用 multiprocessing 模块。下面我将详细介绍这两部分的内容和如何使用它们。

Python 动态加载模块以及多进程问题_加载

问题背景

我正在编写一个 Python 包,它从配置文件中读取模块列表(以及辅助数据)。然后,我想遍历每个动态加载的模块,并调用其中的 do_work() 函数,该函数会生成一个新进程,以便代码在单独的进程中异步运行。

目前,我在主脚本的开头导入了所有已知模块的列表——我觉得这是一个讨厌的 hack,而且不灵活,而且维护起来也很痛苦。

以下是生成进程的函数。我希望在遇到模块时修改它以动态加载该模块。字典中的键是包含代码的模块的名称:

def do_work(work_info):
  for (worker, dataset) in work_info.items():
    # 在此处导入变量 worker 定义的模块...

    # [Edit] 不再使用线程,想在此处异步生成进程...

    #t = threading.Thread(target=worker.do_work, args=[dataset])
    # 我不会将生成的后代守护进程化,因为它们在关闭时需要清理。
    # 由于线程将持有资源
    #t.daemon = True
    #t.start()

问题 1

当我按照上面写的方式在脚本中调用该函数时,会收到以下错误:

AttributeError: 'str' object has no attribute 'do_work'

这是有道理的,因为字典键是一个字符串(要导入的模块的名称)。

当我添加以下语句时:

import worker

在生成线程之前,会收到以下错误:

ImportError: No module named worker

这一点很奇怪,因为使用的是变量名而不是它所保存的值——当我打印该变量时,会得到预期值,这是怎么回事?

问题 2

正如我在注释部分提到的,我知道生成的后代中的 do_work() 函数需要在自身成功完成或者捕获到未处理的异常之后进行清理。我的理解是编写一个 clean_up 函数,在 do_work() 成功完成或者捕获到未处理的异常时调用该函数——我是否还需要做更多的事情来确保资源不会泄露或使操作系统进入不稳定状态?

问题 3

如果我注释掉 t.daemon 标志语句,代码还会异步运行吗?由生成的后代执行的工作非常密集,我不想等到一个后代完成后才能生成另一个后代。顺便说一句,我知道 Python 中的线程实际上是一种时间共享/切片——这没关系。

最后,还有没有一种更好(更 Pythonic)的方法来做我想做的事情?

[Edit]

在阅读了有关 Python 的 GIL 和 Python 中的线程(啊哼——hack)的更多内容之后,我认为最好使用单独的进程(至少 IIUC,如果有多个进程,脚本可以利用它们),所以我将生成新进程而不是线程。

我有一些生成进程的示例代码,但它有点简单(使用 lambda 函数)。我想知道如何扩展它,以便它能够处理加载的模块中的运行函数(就像我上面做的那样)。

以下是我的代码片段:

def do_mp_bench():
    q = mp.Queue() # 不仅线程安全,而且“进程安全”
    p1 = mp.Process(target=lambda: q.put(sum(range(10000000))))
    p2 = mp.Process(target=lambda: q.put(sum(range(10000000)))) 
    p1.start()
    p2.start()
    r1 = q.get()
    r2 = q.get()
    return r1 + r2

我该如何修改它来处理模块的字典并在新进程中运行每个加载模块中的 do_work() 函数?

解决方案

答案 1:使用 import().

答案 2:为什么不在 do_work() 函数的末尾进行清理呢?

答案 3:据我所知,守护线程只意味着程序不会自动等待该线程结束。

以下内容经过修改,可利用 import() 文档此处 和对请求的多处理模块的利用,文档此处。这尚未经过测试。

def do_work(work_info):
    q = mp.Queue()
    for (worker, dataset) in work_info.items():
      xworker = __import__(worker)
      p = mp.Process(target=xworker.do_work, args=dataset).start()
      q.put(p)
    while not q.empty():
      r = q.get()

注意事项

  • 当使用 multiprocessing 模块时,确保 if __name__ == "__main__": 守护在代码最外层,以避免子进程意外执行不该执行的代码。
  • 动态加载模块时,确保模块的路径已经包含在 sys.path 中,或者模块位于合适的位置。

通过上述方法,你可以有效地结合动态模块加载和多进程技术,以应对更为复杂的应用场景。