我正在我编写的一些分析代码之上构建一个接口,该接口执行一些SQL并处理查询结果。我想向用户展示此分析代码中的许多事件,这些事件记录在日志中。因为分析代码运行时间很长,并且因为我不想让UI阻塞,所以到目前为止,我已经通过将分析函数放入其自己的线程中来完成此操作。
我现在拥有的简化示例(完整的脚本):
import sys
import time
import logging
from PySide2 import QtCore, QtWidgets
def long_task():
logging.info('Starting long task')
time.sleep(3) # this would be replaced with a real task
logging.info('Long task complete')
class LogEmitter(QtCore.QObject):
sigLog = QtCore.Signal(str)
class LogHandler(logging.Handler):
def __init__(self):
super().__init__()
self.emitter = LogEmitter()
def emit(self, record):
msg = self.format(record)
self.emitter.sigLog.emit(msg)
class LogDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
log_txt = QtWidgets.QPlainTextEdit(self)
log_txt.setReadOnly(True)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(log_txt)
self.setWindowTitle('Event Log')
handler = LogHandler()
handler.emitter.sigLog.connect(log_txt.appendPlainText)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)
class Worker(QtCore.QThread):
results = QtCore.Signal(object)
def __init__(self, func, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
results = self.func(*self.args, **self.kwargs)
self.results.emit(results)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(widget)
start_btn = QtWidgets.QPushButton('Start')
start_btn.clicked.connect(self.start)
layout.addWidget(start_btn)
self.setCentralWidget(widget)
self.log_dialog = LogDialog()
self.worker = None
def start(self):
if not self.worker:
self.log_dialog.show()
logging.info('Run Starting')
self.worker = Worker(long_task)
self.worker.results.connect(self.handle_result)
self.worker.start()
def handle_result(self, result=None):
logging.info('Result received')
self.worker = None
if __name__ == '__main__':
app = QtWidgets.QApplication()
win = MainWindow()
win.show()
sys.exit(app.exec_())
这很好,除了我需要能够允许用户停止执行分析代码。我读过的所有内容都表明没有办法很好地中断线程,因此使用multiprocessing
库似乎是可行的方法(无法重新编写分析代码以允许定期轮询,因为大部分时间都花在等待查询返回结果上)。通过使用multiprocessing.Pool
和apply_async
以不阻塞UI的方式执行分析代码,获得相同的功能非常容易。
例如从上方将MainWindow
替换为:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(widget)
start_btn = QtWidgets.QPushButton('Start')
start_btn.clicked.connect(self.start)
layout.addWidget(start_btn)
self.setCentralWidget(widget)
self.log_dialog = LogDialog()
self.pool = multiprocessing.Pool()
self.running = False
def start(self):
if not self.running:
self.log_dialog.show()
logging.info('Run Starting')
self.pool.apply_async(long_task, callback=self.handle_result)
def handle_result(self, result=None):
logging.info('Result received')
self.running = False
但是我似乎无法弄清楚如何从子进程中检索日志输出并将其传递给父进程以更新日志对话框。我已经阅读了有关此问题的每一个SO问题,以及有关如何处理从多个进程写入单个日志文件的食谱示例,但是我无法全神贯注于如何使这些想法适应我的想法。我想在这里做。
所以试图弄清楚为什么我看到的行为与我添加的@eyllanesc不同:
logger = logging.getLogger()
print(f'In Func: {logger} at {id(logger)}')
和
logger = logging.getLogger()
print(f'In Main: {logger} at {id(logger)}')
分别到long_task
和Mainwindow.start
。当我运行main.py
时,我得到:
In Main: <RootLogger root (INFO)> at 2716746681984
In Func: <RootLogger root (WARNING)> at 1918342302352
这似乎是this SO question
中描述的内容使用Queue
和QueueHandler
作为解决方案的想法似乎类似于@eyllanesc的原始解决方案
答案 0 :(得分:1)
信号不会在进程之间传输数据,因此在这种情况下,必须使用Pipe然后发出信号:
# other imports
import threading
# ...
class LogHandler(logging.Handler):
def __init__(self):
super().__init__()
self.r, self.w = multiprocessing.Pipe()
self.emitter = LogEmitter()
threading.Thread(target=self.listen, daemon=True).start()
def emit(self, record):
msg = self.format(record)
self.w.send(msg)
def listen(self):
while True:
try:
msg = self.r.recv()
self.emitter.sigLog.emit(msg)
except EOFError:
break
# ...
答案 1 :(得分:1)
万一有人迷路了,使用QueueHandler
和QueueListener
会导致在Windows上也可以使用的解决方案。从this answer to a similar question:
import logging
import sys
import time
import multiprocessing
from logging.handlers import QueueHandler, QueueListener
from PySide2 import QtWidgets, QtCore
def long_task():
logging.info('Starting long task')
time.sleep(3) # this would be replaced with a real task
logging.info('Long task complete')
def worker_init(q):
qh = QueueHandler(q)
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(qh)
class LogEmitter(QtCore.QObject):
sigLog = QtCore.Signal(str)
class LogHandler(logging.Handler):
def __init__(self):
super().__init__()
self.emitter = LogEmitter()
def emit(self, record):
msg = self.format(record)
self.emitter.sigLog.emit(msg)
class LogDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.log_txt = QtWidgets.QPlainTextEdit(self)
self.log_txt.setReadOnly(True)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.log_txt)
self.setWindowTitle('Event Log')
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QHBoxLayout(widget)
start_btn = QtWidgets.QPushButton('Start')
start_btn.clicked.connect(self.start)
layout.addWidget(start_btn)
self.setCentralWidget(widget)
self.log_dialog = LogDialog()
self.running = False
# sets up handler that will be used by QueueListener
# which will update the LogDialoag
handler = LogHandler()
handler.emitter.sigLog.connect(self.log_dialog.log_txt.appendPlainText)
self.q = multiprocessing.Queue()
self.ql = QueueListener(self.q, handler)
self.ql.start()
# main process should also log to a QueueHandler
self.main_log = logging.getLogger('main')
self.main_log.propagate = False
self.main_log.setLevel(logging.INFO)
self.main_log.addHandler(QueueHandler(self.q))
self.pool = multiprocessing.Pool(1, worker_init, [self.q])
def start(self):
if not self.running:
self.log_dialog.show()
self.main_log.info('Run Starting')
self.pool.apply_async(long_task, callback=self.handle_result)
def handle_result(self, result=None):
time.sleep(2)
self.main_log.info('Result received')
self.running = False
def closeEvent(self, _):
self.ql.stop()
if __name__ == '__main__':
app = QtWidgets.QApplication()
win = MainWindow()
win.show()
sys.exit(app.exec_())