为什么 GUI 响应能力会受到工作线程的影响?
Why is GUI responsiveness impaired by a Worker thread?
我试图理解为什么这个 Worker
线程故意使用相当大量的处理(特别是对这些词典的排序)导致 GUI 线程变得无响应。这是一个 MRE:
from PyQt5 import QtCore, QtWidgets
import sys, time, datetime, random
def time_print(msg):
ms_now = datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')
thread = QtCore.QThread.currentThread()
print(f'{thread}, {ms_now}: {msg}')
def dict_reorder(dictionary):
return {k: v for k, v in sorted(dictionary.items())}
class Sequence(object):
n_sequence = 0
simple_sequence_map = {}
sequence_to_sequence_map = {}
prev_seq = None
def __init__(self):
Sequence.n_sequence += 1
if Sequence.n_sequence % 1000 == 0:
print(f'created sequence {Sequence.n_sequence}')
rand_int = random.randrange(100000)
self.text = str(rand_int)
Sequence.simple_sequence_map[self] = rand_int
if Worker.stop_ordered:
time_print(f'init() A: stop ordered... stopping now')
return
dict_reorder(Sequence.simple_sequence_map)
if Sequence.prev_seq:
Sequence.sequence_to_sequence_map[self] = Sequence.prev_seq
if Worker.stop_ordered:
time_print(f'init() B: stop ordered... stopping now')
return
dict_reorder(Sequence.sequence_to_sequence_map)
Sequence.prev_seq = self
def __lt__(self, other):
return self.text < other.text
class WorkerSignals(QtCore.QObject):
progress = QtCore.pyqtSignal(int)
stop_me = QtCore.pyqtSignal()
class Worker(QtCore.QRunnable):
def __init__(self, *args, **kwargs):
super().__init__()
self.signals = WorkerSignals()
def stop_me_slot(self):
time_print('stop me slot')
Worker.stop_ordered = True
@QtCore.pyqtSlot()
def run(self):
total_n = 30000
Worker.stop_ordered = False
for n in range(total_n):
progress_pc = int(100 * float(n+1)/total_n)
self.signals.progress.emit(progress_pc)
Sequence()
if Worker.stop_ordered:
time_print(f'run(): stop ordered... stopping now, n {n}')
return
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.progress = QtWidgets.QProgressBar()
layout.addWidget(self.progress)
start_button = QtWidgets.QPushButton('Start')
start_button.pressed.connect(self.execute)
layout.addWidget(start_button)
self.stop_button = QtWidgets.QPushButton('Stop')
layout.addWidget(self.stop_button)
w = QtWidgets.QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QtCore.QThreadPool()
self.resize(800, 600)
def execute(self):
self.worker = Worker()
self.worker.signals.progress.connect(self.update_progress)
self.stop_button.pressed.connect(self.worker.stop_me_slot)
self.threadpool.start(self.worker)
def update_progress(self, progress):
self.progress.setValue(progress)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
app.exec_()
在我的机器上,直到大约 12%,GUI 明显没有响应:按钮没有获得它们的“悬停”颜色(浅蓝色)并且似乎无法点击(虽然点击“停止”确实会在许多秒后导致停止)。间歇性地出现可怕的微调器(W10 OS 中的蓝色圆圈)。
12%左右后就可以正常使用按钮了。
我做错了什么?
Python 不能 运行 超过一个 CPU 密集线程。原因是GIL。基本上 Python 线程除了等待 I/O.
什么都做不了
如果你想要 CPU 密集部分,请尝试重写密集部分 using Cython,或使用 multiprocessing
,但这样来回发送数据的时间可能会意义重大。
无法重现,ui 即使资源有限也能保持响应。你试过 运行 它没有调试器吗?
GIL 可能是@9000 所建议的问题。
或者 eventloop 可能充斥着 progress
信号,尝试为每个序列发出少于一个信号。
作为旁注:如果您不每次都使用 dict_reorder
丢弃排序结果,程序会运行得更快。
尝试替换
dict_reorder(Sequence.simple_sequence_map)
和
Sequence.simple_sequence_map = dict_reorder(Sequence.simple_sequence_map)
和
dict_reorder(Sequence.sequence_to_sequence_map)
和
Sequence.sequence_to_sequence_map = dict_reorder(Sequence.sequence_to_sequence_map)
一个非常简单的解决方案是使用基本 time.sleep
使线程“休眠”:即使间隔非常小,它也会为主线程提供足够的时间来处理其事件队列,避免 UI 锁定:
def run(self):
total_n = 30000
Worker.stop_ordered = False
for n in range(total_n):
progress_pc = int(100 * float(n+1)/total_n)
self.signals.progress.emit(progress_pc)
Sequence()
if Worker.stop_ordered:
time_print(f'run(): stop ordered... stopping now, n {n}')
return
time.sleep(.0001)
注意:pyqtSlot
装饰器是无用的,因为它只适用于 QObject 子类(QRunnable 不是);你可以删除它。
我试图理解为什么这个 Worker
线程故意使用相当大量的处理(特别是对这些词典的排序)导致 GUI 线程变得无响应。这是一个 MRE:
from PyQt5 import QtCore, QtWidgets
import sys, time, datetime, random
def time_print(msg):
ms_now = datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')
thread = QtCore.QThread.currentThread()
print(f'{thread}, {ms_now}: {msg}')
def dict_reorder(dictionary):
return {k: v for k, v in sorted(dictionary.items())}
class Sequence(object):
n_sequence = 0
simple_sequence_map = {}
sequence_to_sequence_map = {}
prev_seq = None
def __init__(self):
Sequence.n_sequence += 1
if Sequence.n_sequence % 1000 == 0:
print(f'created sequence {Sequence.n_sequence}')
rand_int = random.randrange(100000)
self.text = str(rand_int)
Sequence.simple_sequence_map[self] = rand_int
if Worker.stop_ordered:
time_print(f'init() A: stop ordered... stopping now')
return
dict_reorder(Sequence.simple_sequence_map)
if Sequence.prev_seq:
Sequence.sequence_to_sequence_map[self] = Sequence.prev_seq
if Worker.stop_ordered:
time_print(f'init() B: stop ordered... stopping now')
return
dict_reorder(Sequence.sequence_to_sequence_map)
Sequence.prev_seq = self
def __lt__(self, other):
return self.text < other.text
class WorkerSignals(QtCore.QObject):
progress = QtCore.pyqtSignal(int)
stop_me = QtCore.pyqtSignal()
class Worker(QtCore.QRunnable):
def __init__(self, *args, **kwargs):
super().__init__()
self.signals = WorkerSignals()
def stop_me_slot(self):
time_print('stop me slot')
Worker.stop_ordered = True
@QtCore.pyqtSlot()
def run(self):
total_n = 30000
Worker.stop_ordered = False
for n in range(total_n):
progress_pc = int(100 * float(n+1)/total_n)
self.signals.progress.emit(progress_pc)
Sequence()
if Worker.stop_ordered:
time_print(f'run(): stop ordered... stopping now, n {n}')
return
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.progress = QtWidgets.QProgressBar()
layout.addWidget(self.progress)
start_button = QtWidgets.QPushButton('Start')
start_button.pressed.connect(self.execute)
layout.addWidget(start_button)
self.stop_button = QtWidgets.QPushButton('Stop')
layout.addWidget(self.stop_button)
w = QtWidgets.QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QtCore.QThreadPool()
self.resize(800, 600)
def execute(self):
self.worker = Worker()
self.worker.signals.progress.connect(self.update_progress)
self.stop_button.pressed.connect(self.worker.stop_me_slot)
self.threadpool.start(self.worker)
def update_progress(self, progress):
self.progress.setValue(progress)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
app.exec_()
在我的机器上,直到大约 12%,GUI 明显没有响应:按钮没有获得它们的“悬停”颜色(浅蓝色)并且似乎无法点击(虽然点击“停止”确实会在许多秒后导致停止)。间歇性地出现可怕的微调器(W10 OS 中的蓝色圆圈)。
12%左右后就可以正常使用按钮了。
我做错了什么?
Python 不能 运行 超过一个 CPU 密集线程。原因是GIL。基本上 Python 线程除了等待 I/O.
什么都做不了如果你想要 CPU 密集部分,请尝试重写密集部分 using Cython,或使用 multiprocessing
,但这样来回发送数据的时间可能会意义重大。
无法重现,ui 即使资源有限也能保持响应。你试过 运行 它没有调试器吗?
GIL 可能是@9000 所建议的问题。
或者 eventloop 可能充斥着 progress
信号,尝试为每个序列发出少于一个信号。
作为旁注:如果您不每次都使用 dict_reorder
丢弃排序结果,程序会运行得更快。
尝试替换
dict_reorder(Sequence.simple_sequence_map)
和
Sequence.simple_sequence_map = dict_reorder(Sequence.simple_sequence_map)
和
dict_reorder(Sequence.sequence_to_sequence_map)
和
Sequence.sequence_to_sequence_map = dict_reorder(Sequence.sequence_to_sequence_map)
一个非常简单的解决方案是使用基本 time.sleep
使线程“休眠”:即使间隔非常小,它也会为主线程提供足够的时间来处理其事件队列,避免 UI 锁定:
def run(self):
total_n = 30000
Worker.stop_ordered = False
for n in range(total_n):
progress_pc = int(100 * float(n+1)/total_n)
self.signals.progress.emit(progress_pc)
Sequence()
if Worker.stop_ordered:
time_print(f'run(): stop ordered... stopping now, n {n}')
return
time.sleep(.0001)
注意:pyqtSlot
装饰器是无用的,因为它只适用于 QObject 子类(QRunnable 不是);你可以删除它。