Skip to content

异步桥接: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

模式拆解

  1. Setup:创建 crossbeam_channel::unbounded(),把 SenderReceiver 存为 Resource
  2. Spawn:clone Sender,随任务一起 move 进 async block
  3. Poll:主线程系统每帧调用 receiver.try_iter(),非阻塞地取出所有已完成的结果
  4. 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 则直接接受实例。