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



编辑:我完全重新构造了这个问题,因为我可以在建立一个表达式后更精确。

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

然而,当我试图改变QLineEdit中的文本时,在工作线程中使用waitForReadyRead时,GUI会被阻塞。如果我使用循环重试和waitForReadyRead较小的超时,GUI不会被阻塞。

正如您所看到的,如果我不连接textChanged(因此函数的名称)SignalQLineEdit一切工作正常,我可以编辑GUI中的文本字段。这可能意味着,只要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())

说明

PySide6(以及PySide2)似乎有一个bug,导致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()

你的worker停止的原因是waitForReadyRead函数阻塞了。

waitForReadyRead()阻塞调用,直到有新的数据可供读取。

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

另外,如果发出ready信号,该函数返回true。

来源: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());
}

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

最新更新