我有一个 PyQt GUI,我用它来在 Python 中启动长时间运行的计算。这是一个最小的例子:
import sys
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QDialog,
QVBoxLayout, QPushButton, QDialogButtonBox)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
button = QPushButton("Start", self)
button.clicked.connect(self.long_task)
self.setGeometry(300, 300, 300, 200)
self.show()
def long_task(self):
dialog = QDialog(self)
vbox = QVBoxLayout(dialog)
label = QLabel("Running...")
button = QDialogButtonBox(QDialogButtonBox.Cancel)
vbox.addWidget(label)
vbox.addWidget(button)
dialog.open()
time.sleep(10) # long task, external function
dialog.close()
app = QApplication(sys.argv)
main = MainWindow()
app.exec_()
在主窗口中,我可以通过单击按钮来启动任务。然后弹出一个模式对话框并开始任务。如果 GUI 被阻塞也没关系(我知道我可以通过将任务放在单独的工作线程中来防止冻结 GUI 线程,但这不是重点)。至关重要的是,我希望能够点击“取消”按钮来终止任务。或者,由于长时间运行的任务始终是 Python 命令,我也可以接受使用 Ctrl+C 终止任务。
我无法更改长时间运行的Python命令:即我无法将其分解成小块并将状态变量与线程结合使用,正如有时建议的那样。另一种方法(按 Ctrl+C)也不起作用,因为 PyQt 似乎没有注册它(即使 Python 解释器在运行任务时应该注册)。
最简单的方法是使用多处理。这将允许您同时运行一个任务(或一组任务)并随时终止处理。但是,请务必阅读编程指南以了解如何有效地使用该模块。特别是,虽然 terminate 方法对于独立任务工作得很好,但它不应该用于使用共享资源的多个任务。
这是一个基于您的示例的简单演示:
import sys
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QDialog,
QVBoxLayout, QPushButton, QDialogButtonBox)
from multiprocessing import Pool
def long_task():
for x in range(10):
print('long task:', x)
time.sleep(1)
return 'finished'
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
button = QPushButton("Start", self)
button.clicked.connect(self.long_task)
self.setGeometry(300, 300, 300, 200)
self.show()
def long_task(self):
dialog = QDialog(self)
vbox = QVBoxLayout(dialog)
label = QLabel("Running...")
button = QDialogButtonBox(QDialogButtonBox.Cancel)
button.rejected.connect(dialog.close)
vbox.addWidget(label)
vbox.addWidget(button)
def callback(msg):
print(msg)
dialog.accept()
pool.apply_async(long_task, callback=callback)
if dialog.exec_() == QDialog.Rejected:
pool.terminate()
print('terminated')
app = QApplication(sys.argv)
main = MainWindow()
app.exec_()
我不明白接受的答案中的代码是如何工作的;当我复制并运行它,然后单击“开始”按钮时,我得到:
$ python3 my_qttest.py
Traceback (most recent call last):
File "C:/msys64/tmp/my_qttest.py", line 34, in long_task
pool.apply_async(long_task, callback=callback)
^^^^
NameError: name 'pool' is not defined. Did you mean: 'Pool'?
请注意,我使用:
$ for ix in "uname -s" "python3 --version"; do echo "$ix: " $($ix); done
uname -s: MINGW64_NT-10.0-19045
python3 --version: Python 3.11.8
然后我尝试在
pool = Pool()
之前/之上添加 def long_task():
- 突然间我得到了一大堆窗口被生成,并且
...
File "C:/msys64/tmp/my_qttest.py", line 14, in <module>
pool = Pool()
^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/context.py", line 119, in Pool
return Pool(processes, initializer, initargs, maxtasksperchild,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/pool.py", line 215, in __init__
self._repopulate_pool()
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/pool.py", line 306, in _repopulate_pool
return self._repopulate_pool_static(self._ctx, self.Process,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/pool.py", line 329, in _repopulate_pool_static
w.start()
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/process.py", line 121, in start
self._popen = self._Popen(self)
^^^^^^^^^^^^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/context.py", line 336, in _Popen
return Popen(process_obj)
^^^^^^^^^^^^^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/popen_spawn_win32.py", line 46, in __init__
prep_data = spawn.get_preparation_data(process_obj._name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/spawn.py", line 164, in get_preparation_data
_check_not_importing_main()
File "C:/msys64/mingw64/lib/python3.11/multiprocessing/spawn.py", line 140, in _check_not_importing_main
raise RuntimeError('''
RuntimeError:
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.
This probably means that you are not using fork to start your
child processes and you have forgotten to use the proper idiom
in the main module:
if __name__ == '__main__':
freeze_support()
...
The "freeze_support()" line can be omitted if the program
is not going to be frozen to produce an executable.
To fix this issue, refer to the "Safe importing of main module"
section in https://docs.python.org/3/library/multiprocessing.html
...
情况变得如此糟糕,甚至 Windows 任务管理器都无法关闭所有生成的 Python3 进程 - 我必须使用 SysInternals Process Explorer (
procexp64.exe
) 才能关闭它们。可恶的。我猜这部分与“全局”函数和具有相同名称的类方法有关(long_task()
)
我发现,至少在这个平台上,你必须注意
if __name__ == '__main__':
的事情,例如运行时错误:在当前进程完成引导阶段之前尝试启动新进程。 · 问题 #609 · DEAP/deap:
您必须在 if name == main 中创建池。
有了这个,这里的答案代码已更改,因此它可以在我的 Python 上运行:
import sys
import time
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QDialog,
QVBoxLayout, QPushButton, QDialogButtonBox)
from multiprocessing import Pool
def mp_long_task():
for x in range(10):
print('long task:', x)
time.sleep(1)
return 'finished'
class MainWindow(QMainWindow):
def __init__(self, pool):
super().__init__()
button = QPushButton("Start", self)
button.clicked.connect(self.long_task)
self.setGeometry(300, 300, 300, 200)
self.show()
self.pool = pool
def long_task(self):
dialog = QDialog(self)
vbox = QVBoxLayout(dialog)
label = QLabel("Running...")
button = QDialogButtonBox(QDialogButtonBox.Cancel)
button.rejected.connect(dialog.close)
vbox.addWidget(label)
vbox.addWidget(button)
def callback(msg):
print(msg)
dialog.accept()
self.pool.apply_async(mp_long_task, callback=callback)
if dialog.exec_() == QDialog.Rejected:
pool.terminate()
print('terminated')
if __name__ == '__main__':
pool = Pool()
app = QApplication(sys.argv)
main = MainWindow(pool)
app.exec_()