异步桥接:channel 模式
后台任务的结果需要回到 ECS 主线程才能生效——因为只有主线程能安全地修改 World。最通用的桥接方式是 channel。
Listing 34-6 展示了完整的 channel 桥接模式:
rust
use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool;
use crossbeam_channel::{Receiver, Sender};
use std::time::Duration;
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.add_systems(Startup, (setup_channel, spawn_tasks.after(setup_channel)))
.add_systems(Update, handle_results)
.run();
}
#[derive(Resource)]
struct TaskChannel {
sender: Sender<String>,
receiver: Receiver<String>,
}
fn setup_channel(mut commands: Commands) {
let (sender, receiver) = crossbeam_channel::unbounded();
commands.insert_resource(TaskChannel { sender, receiver });
info!("异步桥接通道已建立");
}
fn spawn_tasks(channel: Res<TaskChannel>) {
let pool = AsyncComputeTaskPool::get();
for i in 0..5 {
let sender = channel.sender.clone();
pool.spawn(async move {
let delay = Duration::from_millis(200 * (i + 1));
// 模拟异步工作
std::thread::sleep(delay);
let result = format!("任务 {i} 完成(耗时 {delay:?})");
let _ = sender.send(result);
})
.detach();
}
info!("已提交 5 个异步任务");
}
fn handle_results(channel: Res<TaskChannel>, mut count: Local<u32>) {
for msg in channel.receiver.try_iter() {
info!("{msg}");
*count += 1;
if *count == 5 {
info!("全部异步任务已完成");
}
}
}Listing 34-6:异步桥接——channel 把后台任务结果送回 ECS
模式拆解
- Setup:创建
crossbeam_channel::unbounded(),把Sender和Receiver存为 Resource - Spawn:clone
Sender,随任务一起 move 进 async block - Poll:主线程系统每帧调用
receiver.try_iter(),非阻塞地取出所有已完成的结果 - Apply:用结果修改 ECS 状态(生成实体、修改组件等)
这个模式的优势:
- 后台任务不需要访问
World,只需要Sender - 主线程按自己的节奏消费结果,不被后台任务阻塞
- 多个后台任务可以共享同一个 channel
NonSend 资源
有些类型不是 Send 的——比如某些数据库连接、平台特定的 API。Bevy 用 NonSend<T> 标记这类资源:
rust
use bevy::prelude::*;
use std::collections::HashMap;
#[derive(Resource)]
struct LocalDb {
connection: String,
data: HashMap<String, String>,
}
impl LocalDb {
fn new() -> Self {
info!("LocalDb 初始化——仅限主线程访问");
let mut data = HashMap::new();
data.insert("key1".to_string(), "value1".to_string());
data.insert("key2".to_string(), "value2".to_string());
Self {
connection: "sqlite://:memory:".to_string(),
data,
}
}
}
impl FromWorld for LocalDb {
fn from_world(_world: &mut World) -> Self {
Self::new()
}
}
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.insert_non_send_resource(LocalDb::new())
.add_systems(Startup, use_local_db)
.run();
}
fn use_local_db(db: NonSend<LocalDb>) {
info!("连接: {}", db.connection);
for (k, v) in &db.data {
info!(" {k} = {v}");
}
}Listing 34-5:NonSend 资源——仅限主线程访问
NonSend<T> 资源只能在主线程系统中访问。如果一个系统使用了 NonSend 参数,Bevy 会保证它只在主线程上运行,不会被并行调度。这避免了数据竞争,也意味着使用 NonSend 的系统会限制调度的并行度。
init_non_send_resource 需要 FromWorld 实现,insert_non_send_resource 则直接接受实例。