为什么在调用 waitForReadyRead 时 GUI 线程被工作线程阻塞?

Why is GUI thread blocked from worker thread when calling waitForReadyRead?

编辑:我完全重组了问题,因为在建立一个 reprex 之后我可以更加精确。

我想做一些同步网络调用,因此我创建了一个工作线程并将对象移动到线程中。

然而,当我尝试更改 QLineEdit 中的文本时,GUI 在工作线程中使用 waitForReadyRead 时被阻止。如果我使用带有重试的循环和更小的 waitForReadyRead 超时,GUI 不会被阻止。

如您所见,如果我不连接 QLineEdittextChanged(因此函数名称)Signal,一切正常,我可以编辑文本GUI 中的字段。 afaik 的意思是,一旦 GUI 需要处理事件,它就会被阻止。

为什么会这样?

如果 GUI 线程和工作线程不是并发执行的,我的假设是 retries 的循环也会一直阻塞。据我所知,waitForReadyRead 的执行以某种方式阻止了两个线程,或者至少阻止了 GUI 线程中事件循环的执行。

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())

您的 worker 停止的原因是因为函数 waitForReadyRead 正在阻塞。

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

我知道你可以将超时作为参数,但我也有阻塞功能的问题。

此外,如果发出就绪信号,此函数 returns 为真。

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

所以也许,不使用 waitForReadyRead(),而是直接使用就绪信号:

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

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

如果有任何问题,这个可能会有所帮助

说明

PySide6(以及 PySide2)似乎存在一个错误,导致 waitForReadyRead 方法阻塞主线程(或主事件循环),从而导致这种意外行为。在 PyQt 中它工作正常。

解决方法

在这种情况下,一个可能的解决方案是通过 qasync 使用 asyncio:

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()