cx замораживание и многопроцессорная обработка.map_async порождает основной поток вместо функции

#python #multiprocessing #pyside2

Вопрос:

У меня есть многопроцессорное графическое приложение, которое безупречно работает при вызове с помощью python на Mac.
Для запуска моих процессов и func асинхронного запуска функции я использую multiprocessing.pools :

 def worker(self): 
        calulation_promise = self._pool.map_async(func, (self.simulated_values), 
                        callback=self.simFin, error_callback=self.simError)
        return calulation_promise
 

Теперь мне нужно создать исполняемый файл из моего проекта с помощью cx-freeze. Я использую шаблон документации, предоставленный здесь.

 import sys
from cx_Freeze import setup, Executable

# Dependencies are automatically detected, but it might need fine tuning.
# "packages": ["os"] is used as example only
excludes = ["Pyside2.Qt5WebEngineCore.dll", "PySide6"]
build_exe_options = {"packages": ['multiprocessing'], "excludes": excludes}


# base="Win32GUI" should be used only for Windows GUI app
base = None
if sys.platform == "win32":
    base = "Win32GUI"

setup(
    name = "guifoo",
    version = "0.1",
    description = "My GUI application!",
    options = {"build_exe": build_exe_options},
    executables = [Executable("main.py", base=base)]
)
 

К сожалению, когда мое основное приложение вызывает мою worker () функцию, оно всегда запускает основной процесс (который запускает новый графический интерфейс главного окна).
Это означает, что вместо выполнения функции func она каким-то образом запускает основной поток снова и снова (для большей ясности см. Выходные данные).

Рабочий пример:

Примечание: Это рабочий пример для воспроизведения проблемы.

 import sys, os, time, logging, platform, multiprocessing, random
from multiprocessing import Process, Pool, cpu_count, freeze_support
from PySide2.QtWidgets import (QLineEdit, QPushButton, QApplication, QVBoxLayout, QDialog)
from PySide2.QtCore import Signal, QObject
from rich.logging import RichHandler

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)
        logging.info ("Stared gui...")
        # Create widgets
        self.edit = QLineEdit("<empty>")
        self.button = QPushButton("Start worker")
        # Create layout and add widgets
        layout = QVBoxLayout()
        layout.addWidget(self.edit)
        layout.addWidget(self.button)
        self.setLayout(layout)                          # Set dialog layout
        self.button.clicked.connect(self.startWorker)   # Add button signal to greetings slot

    # Greets the user
    def startWorker(self):
        logging.info("Stared worker...")
        tw = ThreadWrapper()
        self.promise = tw.worker()
        tw.signals.finished.connect(self.finished)
    
    def finished(self):
        self.edit.setText(str(self.promise.get()))

class ThreadWrapper():
    def __init__(self):
        self.simulated_values = range(1, 30, 1)
        self._pool = Pool(processes=8)
        self.signals = WorkerSignals()

    def simFin(self, value):
        logging.info("%s" % (value))
        self.signals.finished.emit()
    
    def simError(self, value):
        logging.error("%s" % (value))

    def worker(self):
        
        calulation_promise = self._pool.map_async(func, (self.simulated_values), 
                        callback=self.simFin, error_callback=self.simError)
        return calulation_promise


class WorkerSignals(QObject):
    finished = Signal()

# A function which needs an arbitrary amount of time to finish
def func(value):
    wait = random.randint(1, 5); time.sleep(wait) 
    res = value**value
    print("THREAD: %d*%d = %d; waiting %d" % (value, value, res, wait))
    return res

def main():
    logging.basicConfig(level="DEBUG", format="%(name)s | %(message)s", datefmt="[%X]", handlers=[RichHandler()])
    if platform.system() == "Darwin":
            multiprocessing.set_start_method('spawn')
            os.environ['QT_MAC_WANTS_LAYER'] = '1'
            os.environ['QT_MAC_USE_NSWINDOW'] = '1'
            
    app = QApplication(sys.argv)
    window = Form()
    window.show()  
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
 

Выходы

Использование python

 [20:41:50] INFO     root | Stared gui...                             _main.py:11
[20:41:52] INFO     root | Stared worker...                          _main.py:24
THREAD: 3*3 = 27; waiting 1
THREAD: 4*4 = 256; waiting 3
THREAD: 1*1 = 1; waiting 5
THREAD: 2*2 = 4; waiting 5
[20:41:57] INFO     root | [1, 4, 27, 256]   
 

Использование исполняемого файла

 [20:44:03] INFO     root | Stared gui...                             _main.py:11
[20:44:05] INFO     root | Stared worker...                          _main.py:24
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
[20:44:06] INFO     root | Stared gui...                             _main.py:11
 

Дополнительные сведения:

  • Mac OS Big Sur, версия 11.5.1
  • Python 3.7.4
  • PySide2, версия 5.15.2

Комментарии:

1. Тебе, наверное, стоит позвонить freeze_support main . Вы импортируете его, но на самом деле никогда не называли его.

2. Согласно официальной документации , он действителен только для исполняемых файлов Windows: вызов функции freeze_support() не имеет эффекта при вызове в любой операционной системе, кроме Windows. Поэтому я его не включил.

3. Я понимаю, что это так, но поведение, которое вы описываете, аналогично тому, что freeze_support вы пытаетесь решить. Я не знаком с cx-замораживанием на macOS, так что это может быть не решением, но попробовать стоит, нет? Python почти недавно начал по умолчанию «появляться» в macOS, что больше похоже на то, как всегда должна работать Windows.

4. Да, я понимаю вашу точку зрения, и на самом деле я попробовал это в своем приложении, где эта проблема возникла в первую очередь, но это ничего не изменило.

5. хотя… он уже должен использовать «вилку», потому что «spawn» был обновлением 3.8…. ИДК стоит попробовать mp.set_start_method('fork')