python: 打包好的exe程序(冻结程序)中使用多进程,子进程不能正常执行!

1. 问题

  • 代码如下
import multiprocessing as mp
import os
import time

import FreeSimpleGUI as sg


def worker(number):
    print(f"进程 ID: {os.getpid()} - 正在处理数字: {number}")
    return number * number


def process_numbers(numbers):
    with mp.Pool() as pool:
        results = pool.map(worker, numbers)
    print("结果:", results)


def on_button_click():
    numbers = [1, 2, 3, 4, 5]

    # 在后台进程中处理数据
    def background_process():
        process_numbers(numbers)

    # 使用 threading 来启动后台任务
    import threading
    threading.Thread(target=background_process).start()
    sg.popup("处理已开始!")


def gui():
    layout = [
        [sg.Text("你好,PySimpleGUI!")],
        [sg.Button("点击我")]
    ]

    window = sg.Window("简单 GUI", layout)
    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED:
            break
        elif event == "点击我":
            time.sleep(1)
            on_button_click()

    window.close()


if __name__ == '__main__':
    gui()

  • 未打包的时候可以正常运行, 在使用pyinstaller打包成exe以后出现异常:重复打开多个GUI,且后端子进程没有正常启动;
    在这里插入图片描述

2. 问题分析

  • 引入概念冻结应用:

    • “冻结应用”指的是使用工具(如 PyInstaller、cx_Freeze、py2exe 等)将 Python 脚本打包成独立的可执行文件(exe)的过程。冻结后的应用程序包含了 Python 解释器、所有必要的库和资源,使得应用程序可以在没有 Python 环境的机器上运行。这种打包后的应用程序称为“冻结应用”。
  • 在 Python 中使用 multiprocessing 模块时,有几种不同的启动进程的方式,分别是 spawnforkforkserver。以下是它们的中文解释和使用示例。

      1. spawn
      • 解释spawn 方法会在一个全新的 Python 解释器中启动子进程。它适用于 Windows 和 Unix,但在 Windows 上是默认的启动方法。由于每个子进程都是全新启动的,因此需要重新导入模块和执行初始化代码。

      • 使用示例

        import multiprocessing as mp
        
        def worker():
            print("Worker process")
        
        if __name__ == '__main__':
            mp.set_start_method('spawn')
            p = mp.Process(target=worker)
            p.start()
            p.join()
        
      1. fork
      • 解释fork 方法仅适用于 Unix 系统。它通过使用 os.fork() 创建子进程,子进程会继承父进程的所有资源。由于继承了父进程的资源,所以启动速度快,但也可能会有资源竞争的问题。

      • 使用示例

        import multiprocessing as mp
        
        def worker():
            print("Worker process")
        
        if __name__ == '__main__':
            mp.set_start_method('fork')
            p = mp.Process(target=worker)
            p.start()
            p.join()
        
      1. forkserver
      • 解释forkserver 方法通过启动一个单独的服务器进程,专门用于管理新进程的创建。每次需要新进程时,主进程会向服务器发送请求,由服务器来 fork 新的子进程。这样做可以避免在多线程环境中 fork 的一些问题。

      • 使用示例

        import multiprocessing as mp
        
        def worker():
            print("Worker process")
        
        if __name__ == '__main__':
            mp.set_start_method('forkserver')
            p = mp.Process(target=worker)
            p.start()
            p.join()
        
    • 注意事项

        1. Windows 平台:在 Windows 上,spawn 是默认且唯一的选择,因为 Windows 不支持 forkforkserver
        1. Unix 平台:在 Unix 系统上,可以选择任意一种方法。fork 是默认的,但在某些情况下,spawnforkserver 更适合避免资源竞争和安全性问题。
  • 在多进程编程中,Python 的 multiprocessing 模块会在每个子进程中重新执行启动进程的代码文件。具体来说,当你在 Python 文件中启动新的进程时,每个子进程都会从文件的顶部开始执行代码,直到执行到 if name == ‘main’: 这一行。这一点可以从不打包成exe可以正常运行这点看出是正确的,所以导致这种现象的原因是打包成exe(冻结应用)以后,程序并没有遵守这个规则执行子进程

3. 解决方法

  • 在if name == ‘main’: 这一行下面导入一行代码multiprocessing.freeze_support()
    在这里插入图片描述

  • 这个 freeze_support 函数是为了支持在冻结的应用程序(如通过 PyInstaller 打包的应用程序)中使用 multiprocessing 模块而设计的。当应用程序被冻结后,它需要特殊处理来正确地启动和管理子进程。freeze_support 函数确保子进程在被创建时正确运行相应的代码。

    • 代码展示
    def freeze_support():
        '''
        Run code for process object if this is not the main process
        '''
        if is_forking(sys.argv):
            kwds = {}
            for arg in sys.argv[2:]:
                name, value = arg.split('=')
                if value == 'None':
                    kwds[name] = None
                else:
                    kwds[name] = int(value)
            spawn_main(**kwds)
            sys.exit()
    
    • 函数解析

      • 检查是否为子进程

        if is_forking(sys.argv):
        
      • 这行代码调用 is_forking 函数检查当前进程是否是一个子进程。is_forking 函数会根据 sys.argv 的内容来判断。通常,在创建子进程时,会传递特定的命令行参数来标识该进程是子进程。

      • 解析命令行参数

          kwds = {}
          for arg in sys.argv[2:]:
              name, value = arg.split('=')
              if value == 'None':
                  kwds[name] = None
              else:
                  kwds[name] = int(value)
        
        • 如果确定当前进程是子进程,接下来会解析命令行参数(从 sys.argv[2:] 开始)。这些参数是以 name=value 的形式传递的。解析后,将它们存储在一个字典 kwds 中。如果值是 None 字符串,则将其转换为 None,否则将其转换为整数。
      • 调用 spawn_main 函数

           spawn_main(**kwds)
        
        • 解析完参数后,调用 spawn_main 函数,并将解析的参数作为关键字参数传递给它。spawn_main 是负责处理子进程具体任务的函数。
      • 退出子进程

          sys.exit()
        
        • 在完成子进程的任务后,调用 sys.exit() 退出子进程。
  • 使用示例

     - 假设你有一个多进程应用程序,在主模块中你需要确保在冻结后的环境中正确处理子进程的启动。你可以在主模块的 `if __name__ == '__main__'` 块中调用 `freeze_support` 函数:
    
     ```python
     import sys
     from multiprocessing.spawn import freeze_support, spawn_main, is_forking
     
     def main():
         # 主进程代码
         print("Main process running")
     
     if __name__ == '__main__':
         freeze_support()
         main()
     ```
    

4.总结

freeze_support 函数主要用于处理冻结应用中的多进程支持问题。它检查当前进程是否为子进程,如果是,则解析命令行参数,并调用 spawn_main 函数来执行子进程的任务,最后退出子进程。这对于确保在冻结后的应用程序中正确管理子进程非常重要。

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值