Один поток останавливает работу другого потока pyqt5

Помогите пожалуйста, я новичок в PyQt и понятия не имею в чем дело. Перечитал массу литературы и все равно не понимаю.

Пишу небольшую программку для собственных целей. Суть в том, что она тянет данные из MS SQL SERVER и закидывает их обратно.

В ней есть один очень долгоиграющий запрос, который я хочу поместить в отдельный поток, что бы он не блокировал работу всего приложения.

Сделал два класса в обоих есть функции, которые обращаются к SQL, тянут данные и возвращают результат.

Выглядит это примерно вот так:

  • class MyThread - выполняет две функции из основного класса
  • class MyThread4 - как раз в нем и находится самый длинный запрос, который и должен быть в отдельном потоке, что бы не блокировать работу.

Проблема в том, что при запуске длинного потока MyThread4, он блокирует первый поток MyThread (приложение не зависает и продолжает работать).

Приложение не виснет, но MyThread не может продолжить работу, пока не завершится работа MyThread4.

    import sys
    from PyQt5.QtGui import QIcon
    from PyQt5 import QtCore, QtGui, QtWidgets
    from PyQt5.QtCore import QSize, Qt, QThread
    from PyQt5.uic import loadUi
    from PyQt5.QtWidgets import QDialog, QApplication
    import pyodbc
    from redminelib import Redmine
    from dateutil.relativedelta import relativedelta
    from io import BytesIO
    from PyQt5.QtWidgets import (QApplication, QComboBox, QDialog,
                                 QDialogButtonBox, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout,
                                 QLabel, QLineEdit, QMenu, QMenuBar, QPushButton, QSpinBox, QTextEdit,
                                 QVBoxLayout)
    
 class MyThread(QtCore.QObject):
    
        mysignal1 = QtCore.pyqtSignal()
    
        def __init__(self, parent=None):
            QtCore.QObject.__init__(self, parent)
    
        def run(self):
            self.change_x()
    
        def change_x(self):
            while True:
                print('start')
                application.heavy_job()
                time.sleep(6)
                application.cpu_sql()
      
                self.mysignal1.emit()
    
    
    class MyThread4(QtCore.QObject):
        mysignal = QtCore.pyqtSignal()
    
        def __init__(self, parent=None):
            QtCore.QObject.__init__(self, parent)
    
        def second_work(self):
                print('start signlal')
                # Передача данных из потока через сигнал
                application.index_frag_inf()
    
    
    
    class mywindow(QtWidgets.QMainWindow):
        def __init__(self):
            super(mywindow, self).__init__()
            self.ui = Ui_MainWindow()
            self.server = None
            self.password = None
            self.username = None
            self.ui = Ui_MainWindow()
            self.ui.setupUi(self)
    
            self.worker1 = MyThread()
            self.worker1.moveToThread(self.thread1)
            self.ui.butt_conn.clicked.connect(self.worker1.change_x)
            self.thread1.start()
    
    
    
            self.worker = MyThread4()
            self.worker.moveToThread(self.thread)
            self.ui.startcheckindex.clicked.connect(self.worker.second_work)
            self.thread.start()
    
     self.ui.tableWidget.setColumnCount(6)
            self.ui.tableWidget.setHorizontalHeaderLabels(('Fix', 'Index Type', 'TableName', 'IndexName', 'fragmentation %', 'SQLQuery'))
            self.ui.tableWidget.setColumnWidth(0, 100)
            self.ui.tableWidget.setColumnWidth(1, 100)
            self.ui.tableWidget.setColumnWidth(2, 200)
            self.ui.tableWidget.setColumnWidth(3, 400)
            self.ui.tableWidget.setColumnWidth(4, 80)
            self.ui.tableWidget.setColumnWidth(5, 500)
    
    
    
        def index_frag_inf(self):
            self.ui.tableWidget.clearContents()
            self.cursor.execute(""" 
            Здесь длинный запрос с очень долгим выполнением """)
            self.ui.tableWidget.setRowCount(100)
            tablerow = 0
            for index in self.cursor.fetchall():
                self.ui.tableWidget.setItem(tablerow, 0, QtWidgets.QTableWidgetItem(index[0]))
                self.ui.tableWidget.setItem(tablerow, 1, QtWidgets.QTableWidgetItem(index[1]))
                self.ui.tableWidget.setItem(tablerow, 2, QtWidgets.QTableWidgetItem(index[2]))
                self.ui.tableWidget.setItem(tablerow, 3, QtWidgets.QTableWidgetItem(index[3]))
                self.ui.tableWidget.setItem(tablerow, 4, QtWidgets.QTableWidgetItem(index[4]))
                self.ui.tableWidget.setItem(tablerow, 5, QtWidgets.QTableWidgetItem(index[5]))
                tablerow += 1


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    application = mywindow()
    application.show()
    application.setWindowTitle('LTMConsole')
    sys.exit(app.exec())

Помогите пожалуйста! Не могу понять где и в чем именно проблема. Заранее благодарю и хорошего дня!


Ответы (3 шт):

Автор решения: Sergey Tatarincev

Если таблица реально тяжелая, то самая первая ваша пробелема в

self.cursor.fetchall()

получение всех результатов процедура долгая. замените на итерации .cursor.fetch(1)

и второе (возможно более важное).

цикл обхода, если записей действительно много, заблокирует локальный эвентлуп. Попробуйте добавить в цикл qApp->processEvents() для обработки поступающих сигналов

→ Ссылка
Автор решения: S. Nick

Я попробовал что-то сделать для вас и немного прокомментировал код. Если что-то не понятно - спросите.

import sys
import random

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *

#import pyodbc
#from redminelib import Redmine
#from dateutil.relativedelta import relativedelta
#from io import BytesIO

    
class MyThread(QtCore.QObject):
    mysignal1 = QtCore.pyqtSignal(str, object)                       # !!!
    
    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)
        self._stopped = True
        
    def run(self):
        self._stopped = False
        print('start')
        self.change_x()
    
    def change_x(self):
        while not self._stopped:                          # - True: + self._stopped
            print('MyThread работает')
#-            application.heavy_job()                                # -

#+            cursor.execute(""" ...""")                              # +++ добавите
#+            records = cursor.fetchall()                             # +++ добавите
            records = 'Отработал запрос для heavy_job()'             # !!! уберете
            
            self.mysignal1.emit('heavy_job', records)                # !!!
            QtCore.QThread.msleep(1000 * 6)                          # time.sleep(6)
            
#-            application.cpu_sql()
#+            cursor.execute(""" ...""")                             # +++ добавите 
#+            records = cursor.fetchall()                            # +++ добавите
            records = 'Отработал запрос для cpu_sql()'               # !!! уберете
            
            self.mysignal1.emit('cpu_sql', records)                  # !!!
            QtCore.QThread.msleep(100)              

    def stop(self):
        self._stopped = True
    
    
class MyThread4(QtCore.QObject):
    mysignal = QtCore.pyqtSignal(object)                             # !!!
    
    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)
        self._stopped = True
    
#    def second_work(self):
    def run(self):
        print('start MyThread4')
        count = 0
        self._stopped = False

#+        ...   тут ваш длительный запрос   !!!      
#+        cursor.execute(""" Здесь длинный запрос с очень долгим выполнением """)
#+            records = cursor.fetchone()

# vvv уберете - это имитация длительного запроса
        records = []
        for i in range(5000):
            record = list(map(str, random.sample(range(999), 6)))
            records.append(record)
        QtCore.QThread.msleep(1000 * 3)   
# ^^^   
        # Передача данных из потока через сигнал     
        self.mysignal.emit(records)                                  # !!!
#        application.index_frag_inf()

    def stop(self):
        self._stopped = True
    
   
class MyWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.centralwidget = QWidget()
        self.setCentralWidget(self.centralwidget)
        
        self.tableWidget = QTableWidget(0, 6)
        
        self.textEdit = QTextEdit()
        
        self.startcheckindex = QtWidgets.QPushButton('Start MyThread4')
        self.startcheckindex.clicked.connect(self.handleButton)
        
        self.butt_conn = QtWidgets.QPushButton('Start MyThread')
#        self.butt_conn.clicked.connect(self.worker1.change_x)
        self.butt_conn.clicked.connect(self.butt_connButton)

        self.thread1 = QtCore.QThread(self)                          # +++
        self.worker1 = MyThread()
        self.worker1.moveToThread(self.thread1)
        self.worker1.mysignal1.connect(self.worker1_mysignal1)       # !!!
        
#        self.butt_conn.clicked.connect(self.worker1.change_x)
#        self.thread1.start()
        self.thread1.started.connect(self.worker1.run)               # !!!
        
        self.thread = QtCore.QThread(self)                           # +++
        self.worker = MyThread4()
        self.worker.moveToThread(self.thread)
        self.worker.mysignal.connect(self.index_frag_inf)            # !!!
        
#        self.startcheckindex.clicked.connect(self.worker.second_work)
#        self.thread.start()

        self.thread.started.connect(self.worker.run)                 # !!!

#        self.tableWidget.setColumnCount(6)
        self.tableWidget.setHorizontalHeaderLabels(
            ('Fix', 
             'Index Type', 
             'TableName', 
             'IndexName', 
             'fragmentation %', 
             'SQLQuery'
            )
        )
        self.tableWidget.setColumnWidth(0, 100)
        self.tableWidget.setColumnWidth(1, 100)
        self.tableWidget.setColumnWidth(2, 200)
        self.tableWidget.setColumnWidth(3, 300)
        self.tableWidget.setColumnWidth(4, 80)
        self.tableWidget.setColumnWidth(5, 300)

        layout = QGridLayout(self.centralwidget)
        layout.addWidget(self.tableWidget, 0, 0, 1, 1)
        layout.addWidget(self.textEdit, 0, 1, 1, 1)
        layout.addWidget(self.startcheckindex, 1, 0)
        layout.addWidget(self.butt_conn, 1, 1)

    def handleButton(self):
        if self.thread.isRunning():
            self.worker.stop()
        else:
            self.startcheckindex.setText('Stop MyThread4')
            self.thread.start()
           
    def butt_connButton(self):
        if self.thread1.isRunning():
            self.worker1.stop()
            self.textEdit.append('Stop MyThread')
            self.butt_conn.setText('Start MyThread')
            self.thread1.quit()  
        else:
            self.textEdit.append('Start MyThread')
            
            self.butt_conn.setText('Stop MyThread')
            self.thread1.start()
            
    def worker1_mysignal1(self, text, records):                      # <----
        if text == 'heavy_job':
#            self.ui.heavyjob.setText(str(records))  
            self.textEdit.append(str(records))
        elif text == 'cpu_sql':
#            self.ui.cpupercent.setText(str(records))
            self.textEdit.append(str(records))

    def index_frag_inf(self, records):                               # <----
        self.startcheckindex.setText('Start MyThread4')
        self.thread.quit()
    
#        self.tableWidget.clearContents()
        model = self.tableWidget.model()
        model.removeRows(0, model.rowCount())
        
#        self.ui.tableWidget.setRowCount(100)    void QTableWidget::insertRow(int row)
#        tablerow = 0

        for index, record in enumerate(records):
            self.tableWidget.insertRow(index)
            
            self.tableWidget.setItem(index, 0, QtWidgets.QTableWidgetItem(record[0]))
            self.tableWidget.setItem(index, 1, QtWidgets.QTableWidgetItem(record[1]))
            self.tableWidget.setItem(index, 2, QtWidgets.QTableWidgetItem(record[2]))
            self.tableWidget.setItem(index, 3, QtWidgets.QTableWidgetItem(record[3]))
            self.tableWidget.setItem(index, 4, QtWidgets.QTableWidgetItem(record[4]))
            self.tableWidget.setItem(index, 5, QtWidgets.QTableWidgetItem(record[5]))

    def closeEvent(self, event):
        self.worker.stop()
        self.worker1.stop()
        self.worker.deleteLater()
        self.worker1.deleteLater()
        
        splash = QtWidgets.QSplashScreen()
        splash.setPixmap(QtGui.QPixmap('images/splash.jpg'))
        splash.show()
        splash.showMessage(
            '<h1 style="color:white;"><br><br>Подождите пожалуйста приложение закрывается</h1>', 
            QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter, QtCore.Qt.white)    
        QtCore.QThread.msleep(3000)
        splash.showMessage(
            '<h1 style="color:white;"><br><br>Спасибо, что посетили нас, всего хорошего.</h1>', 
            QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter, QtCore.Qt.white) 
        
        QtCore.QThread.msleep(3000)
        self.thread.quit()
        self.thread1.quit()


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    w = MyWindow()
    w.resize(1100, 300)
    w.show()
    w.setWindowTitle('LTMConsole')
    sys.exit(app.exec())

введите сюда описание изображения

введите сюда описание изображения

→ Ссылка
Автор решения: new_polzovatel_in_programming

Если кто вдруг столкнется с такой проблемой как у меня. ВСЕГДА используйте отдельный курсор для каждого подключения, то есть:

def heavy_job(self): -- функция с каким-то запросом 
 self.conn = conn = pyodbc.connect(
            'DRIVER={ODBC Driver 17 for SQL Server};SERVER=' + self.server + ';DATABASE=' + self.database + ';UID='
            + self.username + ';PWD=' + self.password, autocommit=True)  # запускает соединение с SQL-сервером
    cursor_job = conn.cursor() -- именнь здесь, вы должны задавать индивидуальное название Вашему курсору. 
    cursor_job.execute("""Тут будет какой-то запрос""")
        for heavyjob in cursor_job.fetchall(): и тут уже выводите всю нужную Вам инфу.

Теперь, когда Вы будете использовать отдельные потоки, то питону и sql не надо будет драться за один единственный курсор, что бы выполнить все без ошибок, так как таких функицй как heavy_job у Вас может быть 1000 и 1. И в каждом должно быть индивидуальное название.

Более подробно Вы можете ознакомиться в статье, что я нашел 

https://progi.pro/python-sql-cherez-pyodbc-vipolnenie-zaprosov-cherez-cikl-s-dinamicheskim-predlozheniem-where-11491485

Надеюсь, что я правильно донес мысль и не ввел в заблуждение, но данный момент мне действительно помог решить проблему.

→ Ссылка