为什么在调用 waitForReadyRead 时 GUI 线程被工作线程阻塞?
Why is GUI thread blocked from worker thread when calling waitForReadyRead?
编辑:我完全重组了问题,因为在建立一个 reprex 之后我可以更加精确。
我想做一些同步网络调用,因此我创建了一个工作线程并将对象移动到线程中。
然而,当我尝试更改 QLineEdit
中的文本时,GUI 在工作线程中使用 waitForReadyRead
时被阻止。如果我使用带有重试的循环和更小的 waitForReadyRead
超时,GUI 不会被阻止。
如您所见,如果我不连接 QLineEdit
的 textChanged
(因此函数名称)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()
编辑:我完全重组了问题,因为在建立一个 reprex 之后我可以更加精确。
我想做一些同步网络调用,因此我创建了一个工作线程并将对象移动到线程中。
然而,当我尝试更改 QLineEdit
中的文本时,GUI 在工作线程中使用 waitForReadyRead
时被阻止。如果我使用带有重试的循环和更小的 waitForReadyRead
超时,GUI 不会被阻止。
如您所见,如果我不连接 QLineEdit
的 textChanged
(因此函数名称)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()