EDIT: I completly restructured the question because I can be much more precise after build up a reprex.
I want to do some synchronous network calls, therefore I created a worker thread and moved the object to the thread.
However when I try to change the Text in the QLineEdit
the GUI gets blocked when using the waitForReadyRead
in the worker thread. If I use the loop with retries and a smaller timeout for waitForReadyRead
the GUI does not get blocked.
As you can see, if I do not connect the textChanged
(hence the name of the function) Signal
of the QLineEdit
everything works fine and I can edit the text field in the GUI. Which afaik means, that as soon as the GUI needs to process events, it gets blocked.
Why is this happening?
If the the GUI thread and worker thread are not executed concurrent my assumption is that the loop with retries
would also block for the whole time. At the current state of my knowledge somehow the exectuon of waitForReadyRead
blocks both threads or at least the execution of the event loop in the GUI thread.
form.ui:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1292</width>
<height>791</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Textfield</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_edit">
<property name="text">
<string>Some Text</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QPushButton" name="btn_btn">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Button</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1292</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
main.py:
# This Python file uses the following encoding: utf-8
import os
import sys
from PySide6.QtCore import QFile, QObject, QThread, Slot
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QMainWindow
from pathlib import Path
from PySide6.QtNetwork import QTcpSocket
class WorkerClass(QObject):
def __init__(self):
super().__init__()
@Slot()
def do_work(self):
print("Worker Thread: " + str(QThread.currentThread()))
self._socket = QTcpSocket()
self._socket.connectToHost("example.com", 80)
if self._socket.waitForConnected(5000):
print("Connected")
else:
print("Not Connected")
# none blocking ui
# retries = 1000
# while retries:
# retries -= 1
# if self._socket.waitForReadyRead(50):
# answer = self._socket.readAll()
# break
# elif retries == 0:
# print("Timeout")
# blocking ui for 10 seconds
if self._socket.waitForReadyRead(10000):
print("Answer received")
else:
print("Timeout")
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.__load_ui()
self.ui.btn_btn.clicked.connect(self.start_worker)
self.ui.line_edit.textChanged.connect(self.why_blocks_this_connection)
def __load_ui(self):
loader = QUiLoader()
path = os.fspath(Path(__file__).resolve().parent / "form.ui")
ui_file = QFile(path)
ui_file.open(QFile.ReadOnly)
self.ui = loader.load(ui_file, self)
ui_file.close()
def show(self):
self.ui.show()
@Slot()
def start_worker(self):
print("GUI Thread: " + str(QThread.currentThread()))
self._worker = WorkerClass()
self._network_thread = QThread()
self._network_thread.started.connect(self._worker.do_work)
self._worker.moveToThread(self._network_thread)
self._network_thread.start()
def why_blocks_this_connection(self, new_val):
print(new_val)
if __name__ == "__main__":
app = QApplication([])
widget = MainWindow()
widget.show()
sys.exit(app.exec())
There seems to be a bug with PySide6 (and also PySide2) that causes the waitForReadyRead method to block the main thread (or the main eventloop) causing this unexpected behavior. In PyQt it works correctly.
In this case a possible solution is to use asyncio through qasync:
import asyncio
import os
import sys
from pathlib import Path
from PySide6.QtCore import QFile, QIODevice, QObject, Slot
from PySide6.QtWidgets import QApplication
from PySide6.QtUiTools import QUiLoader
import qasync
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class Worker(QObject):
async def do_work(self):
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection("example.com", 80), timeout=5.0
)
except Exception as e:
print("Not Connected")
return
print("Connected")
# writer.write(b"Hello World!")
try:
data = await asyncio.wait_for(reader.read(), timeout=10.0)
except Exception as e:
print("Timeout")
return
print("Answer received")
print(data)
class WindowManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = None
self.__load_ui()
if self.ui is not None:
self.ui.btn_btn.clicked.connect(self.start_worker)
self.ui.line_edit.textChanged.connect(self.why_blocks_this_connection)
def __load_ui(self):
loader = QUiLoader()
path = os.fspath(CURRENT_DIRECTORY / "form.ui")
ui_file = QFile(path)
ui_file.open(QIODevice.ReadOnly)
self.ui = loader.load(ui_file)
ui_file.close()
def show(self):
self.ui.show()
@Slot()
def start_worker(self):
self.worker = Worker()
asyncio.ensure_future(self.worker.do_work())
def why_blocks_this_connection(self, new_val):
print(new_val)
def main():
app = QApplication(sys.argv)
loop = qasync.QEventLoop(app)
asyncio.set_event_loop(loop)
w = WindowManager()
w.show()
with loop:
loop.run_forever()
if __name__ == "__main__":
main()