I'm currently developing a MultiThreading window form application using Python that functions by executing QProcess commands via buttons and signaling start and end with a record that is displayed on the window. However, it always crashes due to unresponsiveness or segmentation faults in a Linux environment, whereas it works fine in a Windows environment. My task takes a long build time, about 8 minutes on average, so I use dummy data to simulate the long run time. Where waitForFinished(-1) is to ensure that the next task is executed only after the current task is completed.
I have used -X faulthandler to locate the position of the Segmentation fault.
Environment Version:
OS: Linux, Python: 3.6.9, PyQt5: 5.15.6
Segmentation Fault:
Thread 0x00007fde5fffe700 (most recent call first):
File .../test.py line 27 in run
File .../test.py line 107 in process_single
File .../test.py line 51 in run
Thread 0x00007fde63ffe700 (most recent call first):
File .../test.py line 27 in run
File .../test.py line 107 in process_single
File .../test.py line 51 in run
Current thread 0x00007fdfd245f740(most recent call first):
File .../test.py line 40 in process_output
File .../test.py line 132 in <module>
My code:
import sys
import threading
from functools import partial
from queue import Queue
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Job(QObject):
task_msg = pyqtSignal(str)
task_started = pyqtSignal()
task_finished = pyqtSignal()
def __init__(self, job_build_cmd):
super().__init__()
self.job_build_cmd = job_build_cmd
self.task_process = None
def run(self):
self.task_process = QProcess()
env = QProcessEnvironment.systemEnvironment()
env.insert('PATH', '/usr/bin:'+env.value('PATH'))
self.task_process.setProcessEnvironment(env)
self.task_process.readyReadStandardOutput.connect(self.process_output)
self.task_process.finished.connect(lambda exitcode: self.process_finished(exitcode))
self.task_process.start('bash', ['-c', self.job_build_cmd])
self.task_process.waitForFinished(-1)
def process_finished(self, exitCode):
self.process_output()
self.task_finished.emit()
if self.task_process is not None:
if self.task_process.isOpen():
self.task_process.close()
self.task_process.deleteLater()
self.task_process = None
def process_output(self):
if self.task_process is not None and self.task_process.isOpen():
line_data = bytes(self.task_process.readAllStandardOutput()).decode('utf-8', errors = 'replace').strip()
for line in line_data.splitlines():
self.task_msg.emit(f'{str(line)}')
class WorkThread(QThread):
def __init__(self, process_single, t_id):
super().__init__()
self.process_single = process_single
self.t_id = t_id
def run(self):
self.process_single(self.t_id)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.job_queue = Queue()
self.work_threads = []
self.stop_job = False
self.lock_get_job = threading.Lock()
self.process = []
self.initUI()
def initUI(self):
self.setGeometry(100, 100, 100, 100)
layout = QVBoxLayout()
self.startbtn = QPushButton('Start')
self.startbtn.clicked.connect(self.run_build)
layout.addWidget(self.startbtn)
centralWidget = QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
def run_build(self):
self.startbtn.setEnabled(False)
tasks = {
'Job 1': 'for i in $(seq 1$((RANDOM%101+50))); do len=$(($RANDOM % 100 + 30)); echo "Random text $i: $(tr -dc A-Za-z0-9 </dev/urandom | head -c $len)"; sleep 0.$((RANDOM % 10 + 1)); done; exit $((RANDOM % 2))'
}
for name, cmd in tasks.items():
job = Job(cmd)
job.task_msg.connect(print)
self.job_queue.put(job)
self.process.append(job)
job.task_finished.connect(partial(self.sync_task_finish, job))
self.create_worker()
def create_worker(self):
self.num_of_worker = 2
self.work_threads =[]
for id in range(self.num_of_worker):
worker = WorkThread(self.process_single, id)
self.work_threads.append(worker)
worker.finished.connect(worker.deleteLater)
worker.start()
def process_single(self, t_id):
while not self.stop_job:
try:
with self.lock_get_job :
if not self.job_queue.empty():
job = self.job_queue.get()
else:
break
if job is not None:
job.task_finished.connect(self.release_process)
job.run()
except Exception as e:
if job is not None:
with self.lock_get_job:
self.job_queue.task_done()
def release_process(self):
with self.lock_get_job :
self.job_queue.task_done()
def sync_task_finish(self, job):
with self.lock_get_job :
if job in self.process:
self.process.remove(job)
if len(self.process)==0:
self.job_finish()
def job_finish(self):
if self.job_queue.empty():
self.startbtn.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
I have attempted to use deleteLater and close to release resources, as well as employing Lock to try to solve the problem, QMetaObject.invokeMethod and @pyqtSlot to replace partial,but none of these solutions have been effective.
I have reduced unnecessary UI displays to ensure the integrity of QProcess and the process of resource release.
I hope it can execute all tasks in the queue properly without the UI crashing.