在GTK4事件循环中添加超时接收蓝牙设备的问题排查
问题排查:Rust+GTK4+bluer异步蓝牙设备枚举无响应
问题描述
使用Rust结合GTK4开发自定义蓝牙客户端时,遇到异步蓝牙设备枚举任务完全未执行的问题:
- 基于bluer库的设备枚举逻辑在独立
async main程序中运行正常,但移入GTK项目后,无法向Arc<Mutex<Vec<String>>>共享队列推送设备名称。 - 主线程通过
glib::timeout_add_local每5秒尝试读取队列,但始终无数据,断点调试发现异步任务根本未启动。
原问题代码
pub fn build_ui(app: Application) -> Application { app.connect_activate(move |app| { let mut queue = Arc::new(Mutex::new(Vec::<String>::new())); let mlayout = Grid::builder() .height_request(700) .width_request(200) .build(); let text_buf = TextBuffer::new(None); let text_view = TextView::builder() .buffer(&text_buf) .width_request(200) .height_request(600) .vexpand(true) .build(); let mwindow = ApplicationWindow::builder() .application(app) .resizable(false) .width_request(200) .height_request(700) .child(&mlayout) .build(); let dev_list = Frame::builder() .height_request(700) .width_request(200) .vexpand(true) .child(&text_view) .build(); let title = Label::builder() .label("Device List") .css_name("Title") .width_request(200) .height_request(50) .justify(gtk4::Justification::Center) .build(); mlayout.attach(&title, 0, 0, 200, 50); mlayout.attach(&dev_list, 0, 50, 200, 1); mwindow.init_layer_shell(); mwindow.set_anchor(Edge::Right, true); mwindow.set_anchor(Edge::Top, true); mwindow.present(); let txqueue = Arc::clone(&queue); thread::spawn(move || async { blue_init(txqueue).await; }); gtk4::glib::timeout_add_local(Duration::from_secs(5), move || { match queue.clone().try_lock() { Ok(dev) => { println!("{:#?}", dev); drop(dev); ControlFlow::Continue } _ => ControlFlow::Continue, } }); }); app } pub async fn blue_init(queue: Arc<Mutex<Vec<String>>>) { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); runtime.spawn(async move { let session = Session::new().await.unwrap(); let adapter = session.default_adapter().await.unwrap(); let discover = adapter.discover_devices().await.unwrap(); pin_mut!(discover); while let Some(evt) = discover.next().await { match evt { AdapterEvent::DeviceAdded(addr) => { let device = adapter.device(addr).unwrap(); let name = device.alias().await.unwrap(); match queue.try_lock() { Ok(mut data) => { data.push(name); drop(data); } _ => {} } } _ => {} } } }); }
问题根源与修复方案
1. 异步任务未被驱动执行
原代码中用std::thread::spawn启动了一个返回Future的闭包,但普通线程不会自动驱动Future执行,导致blue_init(txqueue).await完全没运行。
修复方式:
使用Tokio的spawn(需确保全局已初始化Tokio Runtime),或在普通线程中用block_on阻塞运行Future:
// 方案1:使用全局Tokio Runtime let txqueue = Arc::clone(&queue); tokio::spawn(async move { blue_init(txqueue).await; }); // 方案2:在独立线程中创建并阻塞Runtime let txqueue = Arc::clone(&queue); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(blue_init(txqueue)); });
2. 嵌套Tokio Runtime导致任务提前退出
blue_init内部再次创建了Tokio Runtime,且创建后仅spawn任务就直接返回,Runtime会在函数结束时立即销毁,内部的设备发现任务根本没机会执行。
修复方式:
移除内部Runtime创建,直接在传入的异步上下文中执行任务:
pub async fn blue_init(queue: Arc<Mutex<Vec<String>>>) { let session = Session::new().await.unwrap(); let adapter = session.default_adapter().await.unwrap(); let discover = adapter.discover_devices().await.unwrap(); pin_mut!(discover); while let Some(evt) = discover.next().await { match evt { AdapterEvent::DeviceAdded(addr) => { let device = adapter.device(addr).unwrap(); let name = device.alias().await.unwrap(); // 改用lock()阻塞等待锁,避免丢失设备数据 if let Ok(mut data) = queue.lock() { data.push(name); } } _ => {} } } }
3. Mutex锁定逻辑优化
原代码使用try_lock会在锁被占用时直接放弃,可能丢失设备信息;同时主线程中克隆Arc再锁定属于冗余操作。
修复主线程超时回调:
gtk4::glib::timeout_add_local(Duration::from_secs(5), move || { // 直接锁定原Arc,使用unwrap或优雅处理错误 if let Ok(dev) = queue.lock() { println!("{:#?}", dev); } ControlFlow::Continue });
额外注意事项
- 确保项目依赖中已正确引入Tokio(需启用
rt-multi-thread等必要特性)。 - GTK的UI操作必须在主线程执行,如果后续需要将设备列表更新到TextView,需用
glib::idle_add_local将UI操作调度到主线程。
内容的提问来源于stack exchange,提问作者Zambieslar
相关产品推荐
相关产品推荐

