由于
QThread与您的情况类似,在PyQt中经常存在有关使用的问题,因此以下示例显示了如何在PyQt中正确使用线程。我希望它可以用作类似问题的解答,因此我比平常花了更多的时间准备它。
该示例创建了多个工作对象,这些工作对象在非主线程中执行,并通过Qt的异步信号与主(即GUI)线程进行通信。
import timeimport sysfrom PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlotfrom PyQt5.QtWidgets import QApplication, QPushButton, QTextEdit, QVBoxLayout, QWidgetdef trap_exc_during_debug(*args): # when app raises uncaught exception, print info print(args)# install exception hook: without this, uncaught exception would cause application to exitsys.excepthook = trap_exc_during_debugclass Worker(QObject): """ Must derive from QObject in order to emit signals, connect slots to other signals, and operate in a QThread. """ sig_step = pyqtSignal(int, str) # worker id, step description: emitted every step through work() loop sig_done = pyqtSignal(int) # worker id: emitted at end of work() sig_msg = pyqtSignal(str) # message to be shown to user def __init__(self, id: int): super().__init__() self.__id = id self.__abort = False @pyqtSlot() def work(self): """ Pretend this worker method does work that takes a long time. During this time, the thread's event loop is blocked, except if the application's processEvents() is called: this gives every thread (incl. main) a chance to process events, which in this sample means processing signals received from GUI (such as abort). """ thread_name = QThread.currentThread().objectName() thread_id = int(QThread.currentThreadId()) # cast to int() is necessary self.sig_msg.emit('Running worker #{} from thread "{}" (#{})'.format(self.__id, thread_name, thread_id)) for step in range(100): time.sleep(0.1) self.sig_step.emit(self.__id, 'step ' + str(step)) # check if we need to abort the loop; need to process events to receive signals; app.processEvents() # this could cause change to self.__abort if self.__abort: # note that "step" value will not necessarily be same for every thread self.sig_msg.emit('Worker #{} aborting work at step {}'.format(self.__id, step)) break self.sig_done.emit(self.__id) def abort(self): self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id)) self.__abort = Trueclass MyWidget(QWidget): NUM_THREADS = 5 # sig_start = pyqtSignal() # needed only due to PyCharm debugger bug (!) sig_abort_workers = pyqtSignal() def __init__(self): super().__init__() self.setWindowTitle("Thread Example") form_layout = QVBoxLayout() self.setLayout(form_layout) self.resize(400, 800) self.button_start_threads = QPushButton() self.button_start_threads.clicked.connect(self.start_threads) self.button_start_threads.setText("Start {} threads".format(self.NUM_THREADS)) form_layout.addWidget(self.button_start_threads) self.button_stop_threads = QPushButton() self.button_stop_threads.clicked.connect(self.abort_workers) self.button_stop_threads.setText("Stop threads") self.button_stop_threads.setDisabled(True) form_layout.addWidget(self.button_stop_threads) self.log = QTextEdit() form_layout.addWidget(self.log) self.progress = QTextEdit() form_layout.addWidget(self.progress) QThread.currentThread().setObjectName('main') # threads can be named, useful for log output self.__workers_done = None self.__threads = None def start_threads(self): self.log.append('starting {} threads'.format(self.NUM_THREADS)) self.button_start_threads.setDisabled(True) self.button_stop_threads.setEnabled(True) self.__workers_done = 0 self.__threads = [] for idx in range(self.NUM_THREADS): worker = Worker(idx) thread = QThread() thread.setObjectName('thread_' + str(idx)) self.__threads.append((thread, worker)) # need to store worker too otherwise will be gc'd worker.moveToThread(thread) # get progress messages from worker: worker.sig_step.connect(self.on_worker_step) worker.sig_done.connect(self.on_worker_done) worker.sig_msg.connect(self.log.append) # control worker: self.sig_abort_workers.connect(worker.abort) # get read to start worker: # self.sig_start.connect(worker.work) # needed due to PyCharm debugger bug (!); comment out next line thread.started.connect(worker.work) thread.start() # this will emit 'started' and start thread's event loop # self.sig_start.emit() # needed due to PyCharm debugger bug (!) @pyqtSlot(int, str) def on_worker_step(self, worker_id: int, data: str): self.log.append('Worker #{}: {}'.format(worker_id, data)) self.progress.append('{}: {}'.format(worker_id, data)) @pyqtSlot(int) def on_worker_done(self, worker_id): self.log.append('worker #{} done'.format(worker_id)) self.progress.append('-- Worker {} DONE'.format(worker_id)) self.__workers_done += 1 if self.__workers_done == self.NUM_THREADS: self.log.append('No more workers active') self.button_start_threads.setEnabled(True) self.button_stop_threads.setDisabled(True) # self.__threads = None @pyqtSlot() def abort_workers(self): self.sig_abort_workers.emit() self.log.append('Asking each worker to abort') for thread, worker in self.__threads: # note nice unpacking by Python, avoids indexing thread.quit() # this will quit **as soon as thread event loop unblocks** thread.wait() # <- so you need to wait for it to *actually* quit # even though threads have exited, there may still be messages on the main thread's # queue (messages that threads emitted before the abort): self.log.append('All threads exited')if __name__ == "__main__": app = QApplication([]) form = MyWidget() form.show() sys.exit(app.exec_())以下是了解PyQt中多线程编程所必需的主要概念:
- Qt线程有自己的事件循环(特定于每个线程)。主线程(也称为GUI线程)也是一个
QThread
,其事件循环由该线程管理。 - 线程之间的信号通过接收线程的事件循环(异步)进行传输。因此,GUI或任何线程的响应能力=处理事件的能力。例如,如果线程在函数循环中处于繁忙状态,则它无法处理事件,因此在函数返回之前,它不会响应来自GUI的信号。
- 如果线程中的辅助对象(方法)可能必须根据来自GUI的信号更改其操作过程(例如,中断循环或等待),则它必须调用
processEvents()
该QApplication
实例。这将允许QThread处理事件,从而响应来自GUI的异步信号调用插槽。请注意,QApplication.instance().processEvents()
似乎processEvents()
在每个线程上调用,如果不希望这样做,那么这QThread.currentThread().processEvents()
是一个有效的选择。 - 调用
QThread.quit()
不会立即退出其事件循环:它必须等待当前正在执行的插槽(如果有)返回。因此,一旦告诉线程退出,您必须在其上等待。所以平时中止工作线程包括信令,但它(通过自定义信号),停止不管它是这样做的:这需要一个GUI对象上的自定义信号,该信号的一个工人插槽的连接,和工人的工作方法必须调用线程的processEvents()
到在工作时允许发射的信号到达插槽。



