Фоновый поток с QThread в PyQt
У меня есть программа, которая взаимодействует с радио, которое я использую через gui, который я написал в PyQt. Очевидно, что одной из основных функций радио является передача данных, но для этого постоянно мне нужно зацикливать записи, что приводит к зависанию графического интерфейса. Так как я никогда не имел дело с резьбой, я попытался избавиться от этих зависаний с помощью QCoreApplication.processEvents(). радио должно спать между передачами, хотя, так что gui все еще висит на основе того, как долго эти спит последний.
есть простой способ исправить это с помощью QThread? Я искал учебники о том, как реализовать многопоточность с PyQt, но большинство из них имеют дело с настройкой серверов и гораздо более продвинутые, чем мне нужно. Честно говоря, мне даже не нужно, чтобы мой поток обновлял что-либо во время его работы, мне просто нужно запустить его, передать в фоновом режиме и остановить его.
6 ответов:
Я создал небольшой пример, который показывает 3 различных и простых способа работы с потоками. Я надеюсь, что это поможет вам найти правильный подход к вашей проблеме.
import sys import time from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread, QThreadPool, pyqtSignal) # Subclassing QThread # http://qt-project.org/doc/latest/qthread.html class AThread(QThread): def run(self): count = 0 while count < 5: time.sleep(1) print("A Increasing") count += 1 # Subclassing QObject and using moveToThread # http://blog.qt.digia.com/blog/2007/07/05/qthreads-no-longer-abstract class SomeObject(QObject): finished = pyqtSignal() def long_running(self): count = 0 while count < 5: time.sleep(1) print("B Increasing") count += 1 self.finished.emit() # Using a QRunnable # http://qt-project.org/doc/latest/qthreadpool.html # Note that a QRunnable isn't a subclass of QObject and therefore does # not provide signals and slots. class Runnable(QRunnable): def run(self): count = 0 app = QCoreApplication.instance() while count < 5: print("C Increasing") time.sleep(1) count += 1 app.quit() def using_q_thread(): app = QCoreApplication([]) thread = AThread() thread.finished.connect(app.exit) thread.start() sys.exit(app.exec_()) def using_move_to_thread(): app = QCoreApplication([]) objThread = QThread() obj = SomeObject() obj.moveToThread(objThread) obj.finished.connect(objThread.quit) objThread.started.connect(obj.long_running) objThread.finished.connect(app.exit) objThread.start() sys.exit(app.exec_()) def using_q_runnable(): app = QCoreApplication([]) runnable = Runnable() QThreadPool.globalInstance().start(runnable) sys.exit(app.exec_()) if __name__ == "__main__": #using_q_thread() #using_move_to_thread() using_q_runnable()
очень хороший пример из Matt, я исправил опечатку, а также pyqt4. 8 теперь распространен, поэтому я также удалил фиктивный класс и добавил пример для сигнала dataReady
# -*- coding: utf-8 -*- import sys from PyQt4 import QtCore, QtGui from PyQt4.QtCore import Qt # very testable class (hint: you can use mock.Mock for the signals) class Worker(QtCore.QObject): finished = QtCore.pyqtSignal() dataReady = QtCore.pyqtSignal(list, dict) @QtCore.pyqtSlot() def processA(self): print "Worker.processA()" self.finished.emit() @QtCore.pyqtSlot(str, list, list) def processB(self, foo, bar=None, baz=None): print "Worker.processB()" for thing in bar: # lots of processing... self.dataReady.emit(['dummy', 'data'], {'dummy': ['data']}) self.finished.emit() def onDataReady(aList, aDict): print 'onDataReady' print repr(aList) print repr(aDict) app = QtGui.QApplication(sys.argv) thread = QtCore.QThread() # no parent! obj = Worker() # no parent! obj.dataReady.connect(onDataReady) obj.moveToThread(thread) # if you want the thread to stop after the worker is done # you can always call thread.start() again later obj.finished.connect(thread.quit) # one way to do it is to start processing as soon as the thread starts # this is okay in some cases... but makes it harder to send data to # the worker object from the main gui thread. As you can see I'm calling # processA() which takes no arguments thread.started.connect(obj.processA) thread.finished.connect(app.exit) thread.start() # another way to do it, which is a bit fancier, allows you to talk back and # forth with the object in a thread safe way by communicating through signals # and slots (now that the thread is running I can start calling methods on # the worker object) QtCore.QMetaObject.invokeMethod(obj, 'processB', Qt.QueuedConnection, QtCore.Q_ARG(str, "Hello World!"), QtCore.Q_ARG(list, ["args", 0, 1]), QtCore.Q_ARG(list, [])) # that looks a bit scary, but its a totally ok thing to do in Qt, # we're simply using the system that Signals and Slots are built on top of, # the QMetaObject, to make it act like we safely emitted a signal for # the worker thread to pick up when its event loop resumes (so if its doing # a bunch of work you can call this method 10 times and it will just queue # up the calls. Note: PyQt > 4.6 will not allow you to pass in a None # instead of an empty list, it has stricter type checking app.exec_()
возьмите этот ответ обновлен для PyQt5, python 3.4
используйте это как шаблон для запуска работника, который не принимает данные и возвращает данные, как они доступны для формы.
1-рабочий класс становится меньше и помещается в свой собственный файл worker.py для легкого запоминания и независимого повторного использования программного обеспечения.
2-The main.py файл-это файл, который определяет класс формы графического интерфейса
3-объект потока не является подклассом.
4 - Как объект потока и рабочий объект принадлежат объекту формы
5-шаги процедуры находятся в комментариях.
# worker.py from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot import time class Worker(QObject): finished = pyqtSignal() intReady = pyqtSignal(int) @pyqtSlot() def procCounter(self): # A slot takes no params for i in range(1, 100): time.sleep(1) self.intReady.emit(i) self.finished.emit()и главный файл:
#main.py from PyQt5.QtCore import QThread from PyQt5.QtWidgets import QApplication, QLabel, QWidget, QGridLayout import sys import worker class Form(QWidget): def __init__(self): super().__init__() self.label = QLabel("0") # 1 - create Worker and Thread inside the Form self.obj = worker.Worker() # no parent! self.thread = QThread() # no parent! # 2 - Connect Worker`s Signals to Form method slots to post data. self.obj.intReady.connect(self.onIntReady) # 3 - Move the Worker object to the Thread object self.obj.moveToThread(self.thread) # 4 - Connect Worker Signals to the Thread slots self.obj.finished.connect(self.thread.quit) # 5 - Connect Thread started signal to Worker operational slot method self.thread.started.connect(self.obj.procCounter) # * - Thread finished signal will close the app if you want! #self.thread.finished.connect(app.exit) # 6 - Start the thread self.thread.start() # 7 - Start the form self.initUI() def initUI(self): grid = QGridLayout() self.setLayout(grid) grid.addWidget(self.label,0,0) self.move(300, 150) self.setWindowTitle('thread test') self.show() def onIntReady(self, i): self.label.setText("{}".format(i)) #print(i) app = QApplication(sys.argv) form = Form() sys.exit(app.exec_())
согласно разработчикам Qt, подкласс QThread неверен (см. http://blog.qt.digia.com/blog/2010/06/17/youre-doing-it-wrong/). но эту статью действительно трудно понять (плюс название немного снисходительно). Я нашел лучшее сообщение в блоге, которое дает более подробное объяснение того, почему вы должны использовать один стиль резьбы над другим: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
на мой взгляд, вы, вероятно, никогда не должны подкласс thread с намерением перегрузить метод run. Хотя это действительно работает, вы в основном обходите то, как Qt хочет, чтобы вы работали. Кроме того, вы пропустите такие вещи, как события и правильные потокобезопасные сигналы и слоты. Кроме того, как вы, вероятно, увидите в приведенном выше сообщении в блоге, "правильный" способ потоковой передачи заставляет вас писать более тестируемый код.
вот несколько примеров того, как воспользоваться QThreads в PyQt (Я опубликовал отдельный ответ ниже, который правильно использует QRunnable и включает сигналы/слоты, этот ответ лучше, если у вас есть много асинхронных задач, которые вам нужны для балансировки нагрузки).
import sys from PyQt4 import QtCore from PyQt4 import QtGui from PyQt4.QtCore import Qt # very testable class (hint: you can use mock.Mock for the signals) class Worker(QtCore.QObject): finished = QtCore.pyqtSignal() dataReady = QtCore.pyqtSignal(list, dict) @QtCore.pyqtSlot() def processA(self): print "Worker.processA()" self.finished.emit() @QtCore.pyqtSlot(str, list, list) def processB(self, foo, bar=None, baz=None): print "Worker.processB()" for thing in bar: # lots of processing... self.dataReady.emit(['dummy', 'data'], {'dummy': ['data']}) self.finished.emit() class Thread(QtCore.QThread): """Need for PyQt4 <= 4.6 only""" def __init__(self, parent=None): QtCore.QThread.__init__(self, parent) # this class is solely needed for these two methods, there # appears to be a bug in PyQt 4.6 that requires you to # explicitly call run and start from the subclass in order # to get the thread to actually start an event loop def start(self): QtCore.QThread.start(self) def run(self): QtCore.QThread.run(self) app = QtGui.QApplication(sys.argv) thread = Thread() # no parent! obj = Worker() # no parent! obj.moveToThread(thread) # if you want the thread to stop after the worker is done # you can always call thread.start() again later obj.finished.connect(thread.quit) # one way to do it is to start processing as soon as the thread starts # this is okay in some cases... but makes it harder to send data to # the worker object from the main gui thread. As you can see I'm calling # processA() which takes no arguments thread.started.connect(obj.processA) thread.start() # another way to do it, which is a bit fancier, allows you to talk back and # forth with the object in a thread safe way by communicating through signals # and slots (now that the thread is running I can start calling methods on # the worker object) QtCore.QMetaObject.invokeMethod(obj, 'processB', Qt.QueuedConnection, QtCore.Q_ARG(str, "Hello World!"), QtCore.Q_ARG(list, ["args", 0, 1]), QtCore.Q_ARG(list, [])) # that looks a bit scary, but its a totally ok thing to do in Qt, # we're simply using the system that Signals and Slots are built on top of, # the QMetaObject, to make it act like we safely emitted a signal for # the worker thread to pick up when its event loop resumes (so if its doing # a bunch of work you can call this method 10 times and it will just queue # up the calls. Note: PyQt > 4.6 will not allow you to pass in a None # instead of an empty list, it has stricter type checking app.exec_() # Without this you may get weird QThread messages in the shell on exit app.deleteLater()
в PyQt есть много вариантов для получения асинхронного поведения. Для вещей, которые нуждаются в обработке событий (т. е. QtNetwork и т. д.) Вы должны использовать пример QThread, который я предоставил в своем другом ответе на этот поток. Но для подавляющего большинства ваших потребностей в потоках я думаю, что это решение намного превосходит другие методы.
преимущество этого заключается в том, что QThreadPool планирует ваши экземпляры QRunnable как задачи. Это похоже на шаблон задачи, используемый в Intel ТББ. Это не совсем так элегантно, как мне нравится, но это действительно снимает отличное асинхронное поведение.
Это позволяет вам использовать большую часть мощности резьбы Qt в Python через QRunnable и по-прежнему использовать сигналы и слоты. Я использую этот же код в нескольких приложениях, некоторые из которых делают сотни асинхронных вызовов REST, некоторые из которых открывают файлы или каталоги списков, и лучшая часть использует этот метод, задача Qt балансирует системные ресурсы для мне.
import time from PyQt4 import QtCore from PyQt4 import QtGui from PyQt4.QtCore import Qt def async(method, args, uid, readycb, errorcb=None): """ Asynchronously runs a task :param func method: the method to run in a thread :param object uid: a unique identifier for this task (used for verification) :param slot updatecb: the callback when data is receieved cb(uid, data) :param slot errorcb: the callback when there is an error cb(uid, errmsg) The uid option is useful when the calling code makes multiple async calls and the callbacks need some context about what was sent to the async method. For example, if you use this method to thread a long running database call and the user decides they want to cancel it and start a different one, the first one may complete before you have a chance to cancel the task. In that case, the "readycb" will be called with the cancelled task's data. The uid can be used to differentiate those two calls (ie. using the sql query). :returns: Request instance """ request = Request(method, args, uid, readycb, errorcb) QtCore.QThreadPool.globalInstance().start(request) return request class Request(QtCore.QRunnable): """ A Qt object that represents an asynchronous task :param func method: the method to call :param list args: list of arguments to pass to method :param object uid: a unique identifier (used for verification) :param slot readycb: the callback used when data is receieved :param slot errorcb: the callback used when there is an error The uid param is sent to your error and update callbacks as the first argument. It's there to verify the data you're returning After created it should be used by invoking: .. code-block:: python task = Request(...) QtCore.QThreadPool.globalInstance().start(task) """ INSTANCES = [] FINISHED = [] def __init__(self, method, args, uid, readycb, errorcb=None): super(Request, self).__init__() self.setAutoDelete(True) self.cancelled = False self.method = method self.args = args self.uid = uid self.dataReady = readycb self.dataError = errorcb Request.INSTANCES.append(self) # release all of the finished tasks Request.FINISHED = [] def run(self): """ Method automatically called by Qt when the runnable is ready to run. This will run in a separate thread. """ # this allows us to "cancel" queued tasks if needed, should be done # on shutdown to prevent the app from hanging if self.cancelled: self.cleanup() return # runs in a separate thread, for proper async signal/slot behavior # the object that emits the signals must be created in this thread. # Its not possible to run grabber.moveToThread(QThread.currentThread()) # so to get this QObject to properly exhibit asynchronous # signal and slot behavior it needs to live in the thread that # we're running in, creating the object from within this thread # is an easy way to do that. grabber = Requester() grabber.Loaded.connect(self.dataReady, Qt.QueuedConnection) if self.dataError is not None: grabber.Error.connect(self.dataError, Qt.QueuedConnection) try: result = self.method(*self.args) if self.cancelled: # cleanup happens in 'finally' statement return grabber.Loaded.emit(self.uid, result) except Exception as error: if self.cancelled: # cleanup happens in 'finally' statement return grabber.Error.emit(self.uid, unicode(error)) finally: # this will run even if one of the above return statements # is executed inside of the try/except statement see: # https://docs.python.org/2.7/tutorial/errors.html#defining-clean-up-actions self.cleanup(grabber) def cleanup(self, grabber=None): # remove references to any object or method for proper ref counting self.method = None self.args = None self.uid = None self.dataReady = None self.dataError = None if grabber is not None: grabber.deleteLater() # make sure this python obj gets cleaned up self.remove() def remove(self): try: Request.INSTANCES.remove(self) # when the next request is created, it will clean this one up # this will help us avoid this object being cleaned up # when it's still being used Request.FINISHED.append(self) except ValueError: # there might be a race condition on shutdown, when shutdown() # is called while the thread is still running and the instance # has already been removed from the list return @staticmethod def shutdown(): for inst in Request.INSTANCES: inst.cancelled = True Request.INSTANCES = [] Request.FINISHED = [] class Requester(QtCore.QObject): """ A simple object designed to be used in a separate thread to allow for asynchronous data fetching """ # # Signals # Error = QtCore.pyqtSignal(object, unicode) """ Emitted if the fetch fails for any reason :param unicode uid: an id to identify this request :param unicode error: the error message """ Loaded = QtCore.pyqtSignal(object, object) """ Emitted whenever data comes back successfully :param unicode uid: an id to identify this request :param list data: the json list returned from the GET """ NetworkConnectionError = QtCore.pyqtSignal(unicode) """ Emitted when the task fails due to a network connection error :param unicode message: network connection error message """ def __init__(self, parent=None): super(Requester, self).__init__(parent) class ExampleObject(QtCore.QObject): def __init__(self, parent=None): super(ExampleObject, self).__init__(parent) self.uid = 0 self.request = None def ready_callback(self, uid, result): if uid != self.uid: return print "Data ready from %s: %s" % (uid, result) def error_callback(self, uid, error): if uid != self.uid: return print "Data error from %s: %s" % (uid, error) def fetch(self): if self.request is not None: # cancel any pending requests self.request.cancelled = True self.request = None self.uid += 1 self.request = async(slow_method, ["arg1", "arg2"], self.uid, self.ready_callback, self.error_callback) def slow_method(arg1, arg2): print "Starting slow method" time.sleep(1) return arg1 + arg2 if __name__ == "__main__": import sys app = QtGui.QApplication(sys.argv) obj = ExampleObject() dialog = QtGui.QDialog() layout = QtGui.QVBoxLayout(dialog) button = QtGui.QPushButton("Generate", dialog) progress = QtGui.QProgressBar(dialog) progress.setRange(0, 0) layout.addWidget(button) layout.addWidget(progress) button.clicked.connect(obj.fetch) dialog.show() app.exec_() app.deleteLater() # avoids some QThread messages in the shell on exit # cancel all running tasks avoid QThread/QTimer error messages # on exit Request.shutdown()при выходе из приложения вы хотите, чтобы убедиться, что вы отменить все задачи или приложение будет висеть, пока каждая запланированная задача не будет завершена
основываясь на методах рабочих объектов, упомянутых в других ответах, я решил посмотреть, могу ли я расширить решение, чтобы вызвать больше потоков - в этом случае оптимальное число, которое может запустить машина, и раскрутить несколько рабочих с неопределенным временем завершения. Для этого мне все еще нужно подкласс QThread - но только назначить номер потока и "переопределить" сигналы "готово" и "начато", чтобы включить их номер потока.
я сосредоточился совсем немного на сигналах между основным графическим интерфейсом, потоками и рабочими.
точно так же, другие ответы были болью, чтобы указать не воспитание QThread, но я не думаю, что это реальная проблема. Однако мой код также тщательно уничтожает объекты QThread.
однако я не смог родить рабочие объекты, поэтому кажется желательным отправить им сигнал deleteLater (), либо когда функция потока будет завершена, либо GUI будет уничтожен. У меня был свой собственный код повесить за не делающий это.
еще одно улучшение, которое я чувствовал, было необходимо, было переопределить closeEvent GUI (QWidget) таким образом, что потоки будут проинструктированы выйти, а затем GUI будет ждать, пока все потоки не будут закончены. Когда я играл с некоторыми другими ответами на этот вопрос, я получил QThread уничтоженные ошибки.
возможно, это будет полезно для других. Я, конечно, нашел это полезным упражнением. Возможно, другие будут знать лучший способ для потока объявить об этом тождественность.
#!/usr/bin/env python3 #coding:utf-8 # Author: --<> # Purpose: To demonstrate creation of multiple threads and identify the receipt of thread results # Created: 19/12/15 import sys from PyQt4.QtCore import QThread, pyqtSlot, pyqtSignal from PyQt4.QtGui import QApplication, QLabel, QWidget, QGridLayout import sys import worker class Thread(QThread): #make new signals to be able to return an id for the thread startedx = pyqtSignal(int) finishedx = pyqtSignal(int) def __init__(self,i,parent=None): super().__init__(parent) self.idd = i self.started.connect(self.starttt) self.finished.connect(self.finisheddd) @pyqtSlot() def starttt(self): print('started signal from thread emitted') self.startedx.emit(self.idd) @pyqtSlot() def finisheddd(self): print('finished signal from thread emitted') self.finishedx.emit(self.idd) class Form(QWidget): def __init__(self): super().__init__() self.initUI() self.worker={} self.threadx={} self.i=0 i=0 #Establish the maximum number of threads the machine can optimally handle #Generally relates to the number of processors self.threadtest = QThread(self) self.idealthreadcount = self.threadtest.idealThreadCount() print("This machine can handle {} threads optimally".format(self.idealthreadcount)) while i <self.idealthreadcount: self.setupThread(i) i+=1 i=0 while i<self.idealthreadcount: self.startThread(i) i+=1 print("Main Gui running in thread {}.".format(self.thread())) def setupThread(self,i): self.worker[i]= worker.Worker(i) # no parent! #print("Worker object runningt in thread {} prior to movetothread".format(self.worker[i].thread()) ) self.threadx[i] = Thread(i,parent=self) # if parent isn't specified then need to be careful to destroy thread self.threadx[i].setObjectName("python thread{}"+str(i)) #print("Thread object runningt in thread {} prior to movetothread".format(self.threadx[i].thread()) ) self.threadx[i].startedx.connect(self.threadStarted) self.threadx[i].finishedx.connect(self.threadFinished) self.worker[i].finished.connect(self.workerFinished) self.worker[i].intReady.connect(self.workerResultReady) #The next line is optional, you may want to start the threads again without having to create all the code again. self.worker[i].finished.connect(self.threadx[i].quit) self.threadx[i].started.connect(self.worker[i].procCounter) self.destroyed.connect(self.threadx[i].deleteLater) self.destroyed.connect(self.worker[i].deleteLater) #This is the key code that actually get the worker code onto another processor or thread. self.worker[i].moveToThread(self.threadx[i]) def startThread(self,i): self.threadx[i].start() @pyqtSlot(int) def threadStarted(self,i): print('Thread {} started'.format(i)) print("Thread priority is {}".format(self.threadx[i].priority())) @pyqtSlot(int) def threadFinished(self,i): print('Thread {} finished'.format(i)) @pyqtSlot(int) def threadTerminated(self,i): print("Thread {} terminated".format(i)) @pyqtSlot(int,int) def workerResultReady(self,j,i): print('Worker {} result returned'.format(i)) if i ==0: self.label1.setText("{}".format(j)) if i ==1: self.label2.setText("{}".format(j)) if i ==2: self.label3.setText("{}".format(j)) if i ==3: self.label4.setText("{}".format(j)) #print('Thread {} has started'.format(self.threadx[i].currentThreadId())) @pyqtSlot(int) def workerFinished(self,i): print('Worker {} finished'.format(i)) def initUI(self): self.label1 = QLabel("0") self.label2= QLabel("0") self.label3= QLabel("0") self.label4 = QLabel("0") grid = QGridLayout(self) self.setLayout(grid) grid.addWidget(self.label1,0,0) grid.addWidget(self.label2,0,1) grid.addWidget(self.label3,0,2) grid.addWidget(self.label4,0,3) #Layout parents the self.labels self.move(300, 150) self.setGeometry(0,0,300,300) #self.size(300,300) self.setWindowTitle('thread test') self.show() def closeEvent(self, event): print('Closing') #this tells the threads to stop running i=0 while i <self.idealthreadcount: self.threadx[i].quit() i+=1 #this ensures window cannot be closed until the threads have finished. i=0 while i <self.idealthreadcount: self.threadx[i].wait() i+=1 event.accept() if __name__=='__main__': app = QApplication(sys.argv) form = Form() sys.exit(app.exec_())и рабочий код ниже
#!/usr/bin/env python3 #coding:utf-8 # Author: --<> # Purpose: Stack Overflow # Created: 19/12/15 import sys import unittest from PyQt4.QtCore import QThread, QObject, pyqtSignal, pyqtSlot import time import random class Worker(QObject): finished = pyqtSignal(int) intReady = pyqtSignal(int,int) def __init__(self, i=0): '''__init__ is called while the worker is still in the Gui thread. Do not put slow or CPU intensive code in the __init__ method''' super().__init__() self.idd = i @pyqtSlot() def procCounter(self): # This slot takes no params for j in range(1, 10): random_time = random.weibullvariate(1,2) time.sleep(random_time) self.intReady.emit(j,self.idd) print('Worker {0} in thread {1}'.format(self.idd, self.thread().idd)) self.finished.emit(self.idd) if __name__=='__main__': unittest.main()
Comments