背景:使用 pyside6 + asyncio 库 写一个 gopro 相机管理 GUI 应用时,发现运行 pytest 测试相机连接的代码可以成功,但用 GUI 应用时不行。
🎯 问题根本原因
这是一个典型的 多线程 + asyncio 事件循环冲突 问题:
为什么 pytest 测试正常,但 GUI 失败?
-
pytest 测试环境:
- 单一 asyncio 事件循环
- BLE 回调和 asyncio 代码在同一个线程
queue.put_nowait()直接工作 ✅
-
Qt + qasync GUI 环境:
- 主线程:运行 Qt 事件循环(qasync 集成的 asyncio 事件循环)
- Bleak 线程:BLE 库(Bleak)在单独的线程中处理 BLE 通知
- BLE 回调
_on_notification()在 Bleak 线程中执行 - 直接调用
queue.put_nowait()是非线程安全的,可能导致:- 数据丢失
- 竞态条件
- 事件循环状态不一致 ❌
修复方案
使用 loop.call_soon_threadsafe() 将操作调度到主事件循环线程:
# ❌ 不安全(多线程环境)
self._response_queue.put_nowait(data)
# ✅ 线程安全(通过事件循环调度)
loop.call_soon_threadsafe(self._response_queue.put_nowait, data)这确保了 put_nowait() 在正确的线程中执行。
Question
qt 的事件循环和 asyncio 的事件循环为什么会冲突,我理解 asyncio 的事件循环不应该是全局的吗,还是说 qt 的事件循环不是基于 asyncio 而是自己搞了一套?
你的理解非常正确!让我详细解释一下:
核心问题:两套完全独立的事件循环系统
1. Qt 的事件循环(C++ 实现)
from PySide6.QtWidgets import QApplication
app = QApplication([])
app.exec() # Qt 的事件循环,用 C++ 写的,不是基于 asyncioQt 的事件循环特点:
- 完全独立:用 C++ 实现,完全不依赖 Python 的 asyncio
- 处理 GUI 事件:鼠标点击、键盘输入、定时器、信号槽等
- 阻塞式运行:
app.exec()会阻塞主线程,持续处理 Qt 事件
2. asyncio 的事件循环(Python 实现)
import asyncio
loop = asyncio.get_event_loop()
loop.run_forever() # asyncio 的事件循环,纯 Python 实现asyncio 的事件循环特点:
- Python 标准库:纯 Python 实现
- 处理异步任务:async/await、协程、Future 等
- 也是阻塞式:
loop.run_forever()同样会阻塞主线程
2. 冲突的本质
# ❌ 问题代码示例
import asyncio
from PySide6.QtWidgets import QApplication
app = QApplication([])
# 情况 1:Qt 循环先启动
app.exec() # ← 主线程被 Qt 循环占据,永远不会执行下面的代码
# 下面的代码永远不会执行
loop = asyncio.get_event_loop()
loop.run_forever() # 无法到达# 情况 2:asyncio 循环先启动
loop = asyncio.get_event_loop()
loop.run_forever() # ← 主线程被 asyncio 循环占据
# 下面的代码永远不会执行
app.exec() # 无法到达,GUI 无法响应本质问题:两个事件循环都想独占主线程!
3. 为什么 pytest 能成功?
# pytest 测试代码(简化)
import asyncio
import pytest
@pytest.mark.asyncio
async def test_gopro_connect():
gopro = GoPro()
await gopro.connect() # ✅ 成功
assert gopro.is_connected()
# pytest-asyncio 做的事:
# 1. 创建一个 asyncio 事件循环
# 2. 运行测试函数
# 3. 关闭事件循环
# 没有 Qt,所以没有冲突!4. 深入理解:事件循环的工作原理
Qt 事件循环(伪代码)
// Qt 的 C++ 实现(简化)
void QEventLoop::exec() {
while (!should_exit) {
// 处理 GUI 事件
process_mouse_events();
process_keyboard_events();
process_timers();
process_signals_slots();
// 等待新事件
wait_for_events();
}
}asyncio 事件循环(伪代码)
# asyncio 的 Python 实现(简化)
class EventLoop:
def run_forever(self):
while not self._stopping:
# 处理异步任务
self._run_once()
def _run_once(self):
# 检查就绪的协程
ready = self._ready
for callback in ready:
callback()
# 等待 I/O 事件
self._selector.select(timeout)关键点:两者都在 while 循环中等待和处理事件,都想独占线程!
5. 解决方案对比
方案 A:qasync - 合并两个循环
from qasync import QEventLoop
import asyncio
app = QApplication([])
# qasync 的魔法:用 Qt 的循环驱动 asyncio
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
# 现在只有一个循环在运行(Qt 的),但它会:
# 1. 处理 Qt 事件(原生功能)
# 2. 同时检查和调度 asyncio 任务(qasync 添加的功能)qasync 的实现原理:
# qasync 内部做的事(简化)
class QEventLoop(asyncio.AbstractEventLoop):
def __init__(self, app):
self.app = app
# 创建 Qt 定时器,定期检查 asyncio 任务
self.timer = QTimer()
self.timer.timeout.connect(self._process_asyncio_events)
self.timer.start(10) # 每 10ms 检查一次
def _process_asyncio_events(self):
# 在 Qt 循环中处理 asyncio 任务
self._run_once()方案 B:QThread - 分离两个循环
class AsyncThread(QThread):
def run(self):
# 在新线程中创建独立的 asyncio 循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_forever()
# 主线程:Qt 事件循环
# 工作线程:asyncio 事件循环
# 互不干扰!6. 总结对比表
| 特性 | pytest 环境 | GUI 环境(未处理) | GUI 环境(qasync) |
|---|---|---|---|
| 事件循环 | 仅 asyncio | Qt + asyncio 冲突 | Qt 驱动 asyncio |
| 主线程占用 | asyncio | Qt | Qt(兼容 asyncio) |
| GUI 响应 | 无 GUI | 正常 | 正常 |
| 异步任务 | 正常执行 | 无法执行 | 正常执行 |
7. 验证代码
import asyncio
import sys
from PySide6.QtWidgets import QApplication
# 检查事件循环状态
def check_loops():
print("=== 检查事件循环 ===")
try:
loop = asyncio.get_event_loop()
print(f"asyncio 循环: {loop}")
print(f"是否运行: {loop.is_running()}")
print(f"是否关闭: {loop.is_closed()}")
except RuntimeError as e:
print(f"asyncio 错误: {e}")
app = QApplication.instance()
if app:
print(f"Qt 应用存在: {app}")
else:
print("Qt 应用不存在")
# 测试
check_loops()所以你的理解完全正确:Qt 的事件循环不是基于 asyncio,而是自己用 C++ 实现的一套完全独立的系统。这就是为什么需要 qasync 这样的桥接库来让它们协同工作!