Multithreading in QT richtig anstellen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Multithreading in QT richtig anstellen

Beitragvon Sansch » Freitag 19. Januar 2018, 16:20

Hallo liebe Community,

Ich bin zur Zeit etwas skeptisch gegenüber meiner Multithreading Klassen, es fühlt sich im allgemeinen sehr Ressourcenlastig an und das "spawnen" von neuen Threads wird von Zeit zur Zeit auch immer langsamer. Vorgabe ist es Threads zu starten welche eine Liste von URL's abfragen (können Tausende sein) und den Status an die GUI übergeben, dieses sollte pausiert und auch gestoppt werden können. Die Summe an gleichzeitig laufenden Threads sollte einstellbar sein. Ich habe dazu eine vereinfachte GUI mit den notwendigen Thread Klassen erstellt.

Das ganze läuft unter Qt4 mit Python 2.
Ich bin für jeden Rat und Lösungsansatz dankbar!

Es tut mir im Voraus leid, dass ich so viel Code poste. Ich wüsste allerdings nicht, wie ich das ganze vereinfachen sollte so dass man dies auch testen kann.

GUI

  1. import sys
  2. import requests
  3.  
  4. from PyQt4 import QtCore, QtGui
  5. from random import choice
  6.  
  7. try:
  8.     _fromUtf8 = QtCore.QString.fromUtf8
  9. except AttributeError:
  10.     def _fromUtf8(s):
  11.         return s
  12.  
  13. try:
  14.     _encoding = QtGui.QApplication.UnicodeUTF8
  15.  
  16.     def _translate(context, text, disambig):
  17.         return QtGui.QApplication.translate(context, text, disambig, _encoding)
  18. except AttributeError:
  19.     def _translate(context, text, disambig):
  20.         return QtGui.QApplication.translate(context, text, disambig)
  21.  
  22.  
  23. class Ui_MainWindow(object):
  24.     def __init__(self):
  25.         self.thread_manager = None
  26.         self.url_list = None
  27.  
  28.     def setupUi(self, MainWindow):
  29.         MainWindow.setObjectName(_fromUtf8("MainWindow"))
  30.         MainWindow.resize(410, 301)
  31.         self.centralwidget = QtGui.QWidget(MainWindow)
  32.         self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
  33.         self.result_list = QtGui.QListWidget(self.centralwidget)
  34.         self.result_list.setGeometry(QtCore.QRect(10, 10, 391, 221))
  35.         self.result_list.setObjectName(_fromUtf8("result_list"))
  36.         self.widget = QtGui.QWidget(self.centralwidget)
  37.         self.widget.setGeometry(QtCore.QRect(10, 240, 391, 56))
  38.         self.widget.setObjectName(_fromUtf8("widget"))
  39.         self.verticalLayout = QtGui.QVBoxLayout(self.widget)
  40.         self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
  41.         self.horizontalLayout = QtGui.QHBoxLayout()
  42.         self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
  43.         self.start_button = QtGui.QPushButton(self.widget)
  44.         self.start_button.setObjectName(_fromUtf8("start_button"))
  45.         self.horizontalLayout.addWidget(self.start_button)
  46.         self.stop_button = QtGui.QPushButton(self.widget)
  47.         self.stop_button.setObjectName(_fromUtf8("stop_button"))
  48.         self.horizontalLayout.addWidget(self.stop_button)
  49.         self.verticalLayout.addLayout(self.horizontalLayout)
  50.         self.pause_button = QtGui.QPushButton(self.widget)
  51.         self.pause_button.setObjectName(_fromUtf8("pause_button"))
  52.         self.verticalLayout.addWidget(self.pause_button)
  53.         MainWindow.setCentralWidget(self.centralwidget)
  54.  
  55.         self.retranslateUi(MainWindow)
  56.         self._connect_widgets()
  57.         QtCore.QMetaObject.connectSlotsByName(MainWindow)
  58.  
  59.     def retranslateUi(self, MainWindow):
  60.         MainWindow.setWindowTitle(_translate("MainWindow", "Multithreading Tests", None))
  61.         self.start_button.setText(_translate("MainWindow", "Start", None))
  62.         self.stop_button.setText(_translate("MainWindow", "Stop", None))
  63.         self.pause_button.setText(_translate("MainWindow", "Pause", None))
  64.  
  65.     def _connect_widgets(self):
  66.         self.start_button.clicked.connect(lambda: self._thread_handler(0))
  67.         self.pause_button.clicked.connect(lambda: self._thread_handler(1))
  68.         self.stop_button.clicked.connect(lambda: self._thread_handler(2))
  69.         # Liste mit vielen, vielen, vielen Url's
  70.         self.url_list = open('urlList.txt', 'rb').read().splitlines()
  71.  
  72.     def _thread_handler(self, cmd):
  73.         if cmd == 0:
  74.             self.thread_manager = ThreadManager(self.url_list)
  75.             self.thread_manager.connect(self.thread_manager, QtCore.SIGNAL('WORKER_DONE'), self._add_result)
  76.             self.thread_manager.start()
  77.         elif cmd == 1:
  78.             self.thread_manager.pause()
  79.         elif cmd == 2:
  80.             self.thread_manager.stop()
  81.  
  82.     def _add_result(self, status):
  83.         self.result_list.addItem(status)
  84.  
  85.  
  86. def main():
  87.     app = QtGui.QApplication(sys.argv)
  88.     MainWindow = QtGui.QMainWindow()
  89.     ui = Ui_MainWindow()
  90.     ui.setupUi(MainWindow)
  91.     MainWindow.show()
  92.     sys.exit(app.exec_())
  93.  
  94.  
  95. if __name__ == "__main__":
  96.     main()



Lösung mit QThread's - Threads lassen sich abrupt terminieren. Allerdings ist die Erstellung einer Threadliste wahrscheinlich nicht die eleganteste Lösung.

  1. class ThreadManager(QtCore.QThread):
  2.     def __init__(self, url_list):
  3.         QtCore.QThread.__init__(self)
  4.  
  5.         self.url_list = url_list
  6.         self.thread_pool = []
  7.         self.max_threads = 5
  8.  
  9.         self.paused = False
  10.         self.stopped = False
  11.  
  12.     def run(self):
  13.         while True:
  14.             if not self.paused:
  15.                 if len(self.thread_pool) < self.max_threads:
  16.                     thread = WorkerThread(choice(self.url_list))
  17.                     thread.connect(thread, QtCore.SIGNAL('DONE'), self.return_result)
  18.                     thread.start()
  19.                     self.thread_pool.append(thread)
  20.  
  21.             if self.stopped:
  22.                 break
  23.  
  24.     def return_result(self, result):
  25.         self.emit(QtCore.SIGNAL('WORKER_DONE'), result[0])
  26.         self.thread_pool.remove(result[1])
  27.  
  28.     def stop(self):
  29.         self.stopped = False if self.stopped else True
  30.         # Terminate running QThreads
  31.         [t.terminate() for t in self.thread_pool]
  32.  
  33.     def pause(self):
  34.         self.paused = False if self.stopped else True
  35.  
  36.  
  37. class WorkerThread(QtCore.QThread):
  38.     def __init__(self, url):
  39.         QtCore.QThread.__init__(self)
  40.         self.url = url
  41.  
  42.     def run(self):
  43.         try:
  44.             a = requests.get(self.url).status_code
  45.             self.emit(QtCore.SIGNAL('DONE'), [str(a), self])
  46.         except:
  47.             # Richtiger Weg um ein Thread mit den gleichen Konditionen neu zu starten?
  48.             self.run()

Lösung mit QRunnable's - Threads die einmal im QThreadPool sind können nicht terminiert werden. Somit muss das hinzufügen neuer Threads in den ThreadPool limitiert werden um ein möglichst schnelles pausieren und stoppen zu ermöglichen


  1. class ThreadManager(QtCore.QThread):
  2.     def __init__(self, url_list):
  3.         QtCore.QThread.__init__(self)
  4.  
  5.         self.url_list = url_list
  6.         self.thread_pool = QtCore.QThreadPool()
  7.         self.thread_pool.setMaxThreadCount(5)
  8.         self.queue = 0
  9.  
  10.         self.paused = False
  11.         self.stopped = False
  12.  
  13.     def run(self):
  14.         while True:
  15.             if not self.paused:
  16.                 if self.queue <= 30:  # Limitieren des ThreadPools
  17.                     thread = WorkerThread(choice(self.url_list))
  18.                     thread.signals.result.connect(self.return_result)
  19.                     self.queue += 1
  20.                     self.thread_pool.start(thread)
  21.  
  22.             if self.stopped:
  23.                 break
  24.  
  25.         self.thread_pool.waitForDone()
  26.  
  27.     def return_result(self, result):
  28.         self.queue -= 1
  29.         self.emit(QtCore.SIGNAL('WORKER_DONE'), result)
  30.  
  31.     def stop(self):
  32.         self.stopped = False if self.stopped else True
  33.  
  34.     def pause(self):
  35.         self.paused = False if self.stopped else True
  36.  
  37.  
  38. class WorkerThread(QtCore.QRunnable):
  39.     def __init__(self, url):
  40.         QtCore.QRunnable.__init__(self)
  41.         self.url = url
  42.         self.signals = WorkerSignals()
  43.  
  44.     def run(self):
  45.         try:
  46.             a = requests.get(self.url).status_code
  47.             self.signals.result.emit(str(a))
  48.         except:
  49.             # Richtiger Weg um ein Thread mit den gleichen Konditionen neu zu starten?
  50.             self.run()
  51.  
  52.  
  53. class WorkerSignals(QtCore.QObject):
  54.     finished = QtCore.pyqtSignal()
  55.     error = QtCore.pyqtSignal(tuple)
  56.     result = QtCore.pyqtSignal(object)
  57.     progress = QtCore.pyqtSignal(int)
Benutzeravatar
__deets__
User
Beiträge: 2141
Registriert: Mittwoch 14. Oktober 2015, 14:29

Re: Multithreading in QT richtig anstellen

Beitragvon __deets__ » Freitag 19. Januar 2018, 16:51

Der Selbst-Aufruf von run in WorkeThread ist definitiv ein Bug, weil du damit eine Rekursion ausloest. So etwas waere normalerweise eine while-Schleife mit Abbruchbedingung

Generell ist dein Ansatz ziemlich knoedelig. Und wenn das Ziel der vielen Threads mehr Durchsatz ist, dann auch nicht vor Erfolg gekroent, da Python nicht wirklich multi-threaded ist. Stichwort: global interpreter lock, GIL.

Und normalerweise arbeitet man so, das man eine Reihe von Threads spawnt, die sich eine *Datenstruktur* teilen, ueblicherweise eine Queu. Und da steckt man Arbeitsauftraege rein, und die Ergebnisse in eine zweite, ebenfalls geteilte Datenstruktur. Auf die Art und weise holen sich die Threads ihr Arbeitspaket, und liefern es wieder ab. Statt fuer jede URL einen Thread zu starten (so zumindest sieht das aus).

Generell wuerde ich das anders machen:

- benutz Pythons eigene Threading oder Multiprocessing Mechanismen
- wenn der Durchsatz wirklich maximal sein soll, nutze asyncio
- das der ThreadManager selbst ein Thread ist, ist unnoetig.
- wechsel auf Python3 und vor allem Qt5, Qt4 ist schon jenseits seiner Lebenszeit.
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Re: Multithreading in QT richtig anstellen

Beitragvon Sansch » Freitag 19. Januar 2018, 18:25

Super, vielen dank das du dir die Zeit genommen hast dir den Code anzuschauen.
Ich dachte mir schon dass ich das ganze falsch angehe. Ich spawne in der Tat für jede URL ein neuen Thread.

- benutz Pythons eigene Threading oder Multiprocessing Mechanismen
Spricht etwas prinzipiell gegen QThreads bzw. QRunnables? Oder sprichst du da aus erfahrung?
- wenn der Durchsatz wirklich maximal sein soll, nutze asyncio
Asyncio ist mir bis jetzt noch neu. Werde mich einlesen
- das der ThreadManager selbst ein Thread ist, ist unnoetig.
Kann ich das ganze denn von der GUI Klasse steuern? Friert mir da die GUI nicht ein?
- wechsel auf Python3 und vor allem Qt5, Qt4 ist schon jenseits seiner Lebenszeit.
Tatsache? Habe etwas Angst vor den Wechsel da ich doch größere Veränderungen erwarte.

Da ich auch mit der GIL nicht wirklich vertraut bin. Lockt diese denn nicht nur C code? Purer python code sollte doch threadbar sein oder? Ich meine immerhin sind die Threads ja deutlich schneller als wenn ich nach und nach die URL's im Hauptscript abfrage.

Ich werde mich definitiv mal einlesen und demnächst hier nochmal ein Code posten.
Benutzeravatar
__deets__
User
Beiträge: 2141
Registriert: Mittwoch 14. Oktober 2015, 14:29

Re: Multithreading in QT richtig anstellen

Beitragvon __deets__ » Freitag 19. Januar 2018, 18:37

Dein denken ist genau andersherum als die Realitaet - C-Code ist *nicht* gelockt, Python-Code ist immer nur serialisiert ausfuehrbar.

Die Unterschiede zwischen Qt4 und Qt5 sind jetzt nicht so gewaltig, aber Qt4 wird halt nicht mehr supported. Fuer ein in der Entwicklung befindliches Programm darum die falsche Wahl.

Und das schedulen neuer Threads ist nicht schwergewichtig, darum braucht der Thread-Manager selbst auch nicht ein Thread sein.

Die bordeigenen Mittel zu nutzen hat den Vorteil, das du das ganze auch ohne weiteres standalone testen kannst, und hoehere Portabilitaet hast. Und wenn du *wirklich* nebenlaeufig sein willst, ist multiprocessing deutlich einfacher als drop-in fuer threading als etwas, das auf Qt beruht. Da hast du dann alles von vorne zu machen.
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Re: Multithreading in QT richtig anstellen

Beitragvon Sansch » Freitag 19. Januar 2018, 23:17

Nochmal vielen Dank. Habe nun dank deiner tips und einigen Tutorials aus dem Netz folgendes gebastelt. Fühlt sich viel leichter und besser im handling an.

  1. import sys
  2. import asyncio
  3.  
  4. from aiohttp import ClientSession
  5. from PyQt5 import QtCore, QtGui, QtWidgets
  6. from random import choice
  7.  
  8.  
  9. class MainUI(object):
  10.     def setupUi(self, main_ui):
  11.         main_ui.setObjectName("main_ui")
  12.         main_ui.resize(410, 301)
  13.         self.centralwidget = QtWidgets.QWidget(main_ui)
  14.         self.centralwidget.setObjectName("centralwidget")
  15.         self.result_list = QtWidgets.QListWidget(self.centralwidget)
  16.         self.result_list.setGeometry(QtCore.QRect(10, 10, 391, 221))
  17.         self.result_list.setObjectName("result_list")
  18.         self.widget = QtWidgets.QWidget(self.centralwidget)
  19.         self.widget.setGeometry(QtCore.QRect(10, 240, 391, 56))
  20.         self.widget.setObjectName("widget")
  21.         self.verticalLayout = QtWidgets.QVBoxLayout(self.widget)
  22.         self.verticalLayout.setContentsMargins(0, 0, 0, 0)
  23.         self.verticalLayout.setObjectName("verticalLayout")
  24.         self.horizontalLayout = QtWidgets.QHBoxLayout()
  25.         self.horizontalLayout.setObjectName("horizontalLayout")
  26.         self.start_button = QtWidgets.QPushButton(self.widget)
  27.         self.start_button.setObjectName("start_button")
  28.         self.horizontalLayout.addWidget(self.start_button)
  29.         self.stop_button = QtWidgets.QPushButton(self.widget)
  30.         self.stop_button.setObjectName("stop_button")
  31.         self.horizontalLayout.addWidget(self.stop_button)
  32.         self.verticalLayout.addLayout(self.horizontalLayout)
  33.         self.pause_button = QtWidgets.QPushButton(self.widget)
  34.         self.pause_button.setObjectName("pause_button")
  35.         self.verticalLayout.addWidget(self.pause_button)
  36.         main_ui.setCentralWidget(self.centralwidget)
  37.  
  38.         self.retranslateUi(main_ui)
  39.        
  40.     def retranslateUi(self, main_ui):
  41.         _translate = QtCore.QCoreApplication.translate
  42.         main_ui.setWindowTitle(_translate("main_ui", "Multithreading Tests"))
  43.         self.start_button.setText(_translate("main_ui", "Start"))
  44.         self.stop_button.setText(_translate("main_ui", "Stop"))
  45.         self.pause_button.setText(_translate("main_ui", "Pause"))
  46.         self.bind_widgets()
  47.  
  48.     def bind_widgets(self):
  49.         self.start_button.clicked.connect(lambda: self.thread_handler(0))
  50.         self.pause_button.clicked.connect(lambda: self.thread_handler(1))
  51.         self.stop_button.clicked.connect(lambda: self.thread_handler(2))
  52.  
  53.     def thread_handler(self, cmd):
  54.         if cmd == 0:
  55.             self.thr_manager = ThreadManager()
  56.             self.thr_manager.add_status.connect(self.add_status)
  57.             self.thr_manager.start()
  58.             self.start_button.setEnabled(False)
  59.         elif cmd == 1:
  60.             self.thr_manager.pause()
  61.         elif cmd == 2:
  62.             self.thr_manager.stop()
  63.  
  64.     def add_status(self, s):
  65.         self.result_list.addItem(s)
  66.  
  67.  
  68. class ThreadManager(QtCore.QThread):
  69.     add_status = QtCore.pyqtSignal(str)
  70.  
  71.     def __init__(self):
  72.         QtCore.QThread.__init__(self)
  73.         self.urls = open('urlList.txt').read().splitlines()
  74.         self.tasks = []
  75.         self.paused = False
  76.         self.stopped = False
  77.         self.loop = asyncio.new_event_loop()
  78.  
  79.         self.max_threads = 5
  80.  
  81.     async def fetch(self, url, sem):
  82.         async with ClientSession() as session:
  83.             async with sem:
  84.                 if not self.stopped:
  85.                     while self.paused:
  86.                         asyncio.sleep(0.5)
  87.                         pass
  88.  
  89.                     try:
  90.                         async with session.get(url) as c:
  91.                             self.add_status.emit(str(c.status))
  92.                     except Exception as e:
  93.                         print(e)
  94.                         # Diesmal richtig?
  95.                         self.append_task(url, sem)
  96.  
  97.     async def thread_runner(self, r):
  98.         url = choice(self.urls)
  99.         sem = asyncio.BoundedSemaphore(self.max_threads)
  100.  
  101.         [self.append_task(url, sem) for _ in range(r)]
  102.         await asyncio.gather(*self.tasks)
  103.         # Fertig.
  104.  
  105.     def append_task(self, url, sem):
  106.         task = asyncio.ensure_future(self.fetch(url, sem))
  107.         self.tasks.append(task)
  108.  
  109.     def run(self):
  110.         number = 10000
  111.         asyncio.set_event_loop(self.loop)
  112.         future = asyncio.ensure_future(self.thread_runner(number))
  113.         self.loop.run_until_complete(future)
  114.  
  115.     def pause(self):
  116.         self.paused = False if self.paused else True
  117.  
  118.     def stop(self):
  119.         self.stopped = True
  120.  
  121.  
  122. def main():
  123.     app = QtWidgets.QApplication(sys.argv)
  124.     main_ui = QtWidgets.QMainWindow()
  125.     ui = MainUI()
  126.     ui.setupUi(main_ui)
  127.     main_ui.show()
  128.     sys.exit(app.exec_())
  129.  
  130.  
  131. if __name__ == "__main__":
  132.     main()
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Re: Multithreading in QT richtig anstellen

Beitragvon Sansch » Samstag 20. Januar 2018, 18:01

Nochmal einen Nachtrag zu dem obigen code:
Das erstellen einer aiohttp session für jeden asynchronen Aufruf ist eine schlechte Idee. Das ist sehr.. sehr ... seeehr speicher lastig.

Lösung:
Eine Session für alle requests benutzen. Funktioniert bis jetzt ganz gut.
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Re: Multithreading in QT richtig anstellen

Beitragvon Sansch » Samstag 20. Januar 2018, 19:28

Erneut vor einem neuen Problem zum oben Beispiel.
Die Taskliste die generiert wird, wird unglaublich groß und futtert GB an Speicher.

Bin ich denn gezwungen eine Taskliste zu generieren? Oder gibt es da andere Möglichkeiten?
Sansch
User
Beiträge: 6
Registriert: Freitag 19. Januar 2018, 15:41

Re: Multithreading in QT richtig anstellen

Beitragvon Sansch » Sonntag 21. Januar 2018, 02:41

Und noch mal ich selbst.

Queue war die Lösung. Genau wie deets bereits gesagt hat. Scheint so als hätte ich jetzt den Dreh raus.

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder