背景:使用 pyside6 + asyncio 库 写一个 gopro 相机管理 GUI 应用时,发现运行 pytest 测试相机连接的代码可以成功,但用 GUI 应用时不行。

🎯 问题根本原因

这是一个典型的 多线程 + asyncio 事件循环冲突 问题:

为什么 pytest 测试正常,但 GUI 失败?

  1. pytest 测试环境

    • 单一 asyncio 事件循环
    • BLE 回调和 asyncio 代码在同一个线程
    • queue.put_nowait() 直接工作 ✅
  2. 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++ 写的,不是基于 asyncio

Qt 的事件循环特点:

  • 完全独立:用 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)
事件循环仅 asyncioQt + asyncio 冲突Qt 驱动 asyncio
主线程占用asyncioQtQt(兼容 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 这样的桥接库来让它们协同工作!