0

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())
Enak
  • 546
  • 3
  • 15

2 Answers2

2

Explanation

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.

Workaround

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
0

The reason why your worker stopped is because the function waitForReadyRead is blocking.

waitForReadyRead() blocks calls until new data is available for reading.

I know that you can put a timeout as a parameter, but I had this problem of blocking function too.

Plus, this function returns true if a ready signal is emitted.

source : https://doc.qt.io/qt-5/qserialport.html#waitForReadyRead

So maybe, instead of using waitForReadyRead(), use a ready signal directly :

connect(deviceControl_SerialPort, &QSerialPort::readyRead, this, &Class::readData_slot);

void Class::readData_slot()
{
    qDebug() << "Ready Read" << endl;
    deviceControl_readData.append(deviceControl_SerialPort->readAll());
}

If any troubles, this issue may help

Pommepomme
  • 115
  • 1
  • 11
  • THX for your answer. I know that is the "usual" way but before my big restructure of the question, I mentioned that I have to implement a certain process. It consists out of many, many steps, each step needs to send a tcp packet to one out of two available devices. It is much more easier to write code from "first line to last line" with blocking calls instead of having a big if then else struct and keep track of the states to know which device needs to be communicate with next and what is the next packet. – Enak Aug 06 '21 at 08:26
  • I know that when you wait for ready read, you have to read all the data when the function returns true, like you did in your comments. As you have to use waitForReadyRead, an other way is to call bytesAvailable > 0 to check if there is bytes to read – Pommepomme Aug 06 '21 at 08:36
  • Since you edit your first paragraph: A blocking call, in my understanding, is a call which blocks further execution of the thread. Hence, it should not block other threads. This is exactly the reason why I moved the blocking calls to another thread. – Enak Aug 06 '21 at 08:37
  • I never use sockets with Qt, but I think in some ways, your threads are connected to each other and if one is blocked, the other will wait. (edit because enter key saved the message ) : Did your program continues after the 10000 timeout or is it constantly blocking ? Did you run it on Windows ? There is a known issue about waitForReadyRead timeouts on Windows – Pommepomme Aug 06 '21 at 08:46