为3DS构建AsyncIO执行器

Lobsters Hottest 工具

摘要

本文介绍了在Nintendo 3DS上进行异步编程的必要性,因为其采用协作式多任务处理,并开始解释如何为其构建一个asyncio执行器,重点讨论了Rust中的任务、未来、唤醒器和执行器这些概念。

<p><a href="https://lobste.rs/s/tqxsky/building_asyncio_executor_for_3ds">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/26 15:21

# 为 3DS 构建一个 AsyncIO 执行器(第一部分!) 来源:https://blog.cat-girl.gay/3ds-async-part-one/ *2026 年 5 月 16 日* Nintendo 3DS 是一台写自制软件非常有趣的设备,但有一件事可能很烦人:它的多任务是非抢占式的!在这个小系列中,我们将为 3DS 构建一个 asyncio 执行器。在第一部分中,我们先来看看:为什么要有异步,以及异步到底是什么? ## 什么? 操作系统通常通过**抢占**来实现线程和进程:一个线程只能运行一小段时间,然后会被暂时暂停,以便让下一个线程运行。因此,如果你创建一个非常密集、长期运行的线程,它无法独占整个 CPU,因为操作系统会介入并确保其他线程也能获得 CPU 时间。 ### 但 3DS 是**协作式**的 这意味着线程只有在主动请求暂停时才会被暂停。所以,如果我们的密集长期运行线程从不主动释放 CPU,那么**其他任何东西**都无法运行!当然,我们只需小心谨慎,确保它不会发生,但这很容易忘记。如果我们能在编写代码时用一种方式来建模这个问题,那就太方便了…… ## 直接 await **异步**编程正是这样一种模型!在异步程序中,我们将每部分工作建模为一个`任务`。任务不应该长时间占用 CPU:它应该做一点工作,然后在等待某事发生(例如 TCP 数据包到达)时休眠,然后再做一点工作。我们可以等待的东西称为`Future`。没有异步,这些事情并非不可能做到:只是异步让这一切变得非常明确。这一点在 3DS 上尤其重要,因为平台的行为……有时候不可预测,难以判断什么会导致任务主动让出 CPU。 ``` // 在普通程序中,很难知道这会不会让我们的线程休眠…… socket.read(); // 但在异步中,明确表明它会让我们的任务等待! socket.read().await; ``` 神奇的`.await`表示我们希望任务**休眠直到**`socket.read()`(它本身是一个`Future`)完成。 ## 好的,但这是怎么工作的? 我必须承认:“休眠”这个词用得并不准确。实际上,任务内部并不像线程:它是一个我们反复调用的函数,就像这样: ``` while task.do_some_work() == NotReady {} ``` 所以,如果我们有一个任务要两次从套接字读取,比如: ``` let task = async { socket.read().await; socket.read().await; } ``` 它就会变成这样: ``` let task = { let mut read_operation = socket.read(); while read_operation() == NotReady {} let mut read_operation_two = socket.read(); while read_operation() == NotReady {} } ``` ## 等等!这不过是忙循环而已! 没错:这种异步很糟糕,因为它实际上根本没有让出 CPU!相反,它可能占用更多 CPU 时间,因为它在循环中反复调用同一个函数。我们应该只在**实际**可以完成新工作(例如可以从套接字读取数据)时才调用`do_some_work()`或`read_operation()`函数。如果你曾经接触过**回调**,可能会觉得似曾相识。 ``` socket.on_has_data(|| { read(); }); ``` 然而,回调很快会陷入地狱,而且实际上是由缩进行业发明的,目的是多卖 TAB 键。 ## 唤醒我(从内部唤醒我) 相反,Rust 的异步系统使用**唤醒器**和**执行器**。`执行器`负责管理我们的任务:它负责调用`do_some_work()`函数。但除非我们明确请求,否则它不会调用。`唤醒器`是一种抽象,用来“请求执行器再次调用我们的函数,拜托拜托”。 ``` fn do_some_work() { // 如果数据已准备好,我们可以直接读取并立即返回! if socket.has_data() { return Ready(socket.read()) } else { // 否则,我们向套接字注册自己:当它有数据时,它会告诉执行器再次调用我们。 socket.on_has_data(|| executor.wake_me_up()); return NotReady // 我们还没准备好,所以先返回! } } fn executor() { if do_some_work() == Ready(data) { return data }; // 我们先调用一次,以防它已经有数据,或者让它注册自己以便稍后唤醒。 while let Some(wake_request) = wake_requests.receive() { // 我们可以使用信道或类似的东西来接收唤醒请求 do_some_work(); } } ``` ## 好吧,让我们真正实现它 到目前为止所有示例都非常伪代码风格。让我们实际尝试构建一个最小可行的执行器,同时实现一个极其简化的`sleep()`。 ``` pub struct Sleep { registered: bool } impl Future for Sleep { type Output = (); // 这里我们用标准库的 Poll 类型,它有两个变体:Ready(val) 或 Pending。 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if !self.registered { // 这是我们第一次被调用! let waker = context.waker().clone(); std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(5)); waker.wake(); // 请求执行器再次调用 Sleep future }); self.registered = true; // 标记我们已经请求五秒后唤醒 return Poll::Pending // 我们还没完成! } else { // 这不是第一次被调用,所以一定是因为我们的唤醒器唤醒了我们! return Poll::Ready(()) } } } // 我们用一些整数来标识任务! pub struct TaskId(u32); // 由于某些原因(这里不深入),future 有非常具体的内存布局要求,这使得把任务放在堆上更容易。 // 这里我们使用了 `futures` crate 中的 BoxFuture 类型别名。 pub type Task = BoxFuture<'static, ()>; // 我们将使用一个 mpsc 信道来处理唤醒请求,所以我们的 `Waker` 就是一个信道发送者加上要唤醒的 id pub struct TaskWaker { id: TaskId, sender: std::sync::mpsc::SyncSender } // 我们需要实现 Wake trait 来创建 Waker:它定义了在我们的设置中如何唤醒一个任务 impl Wake for TaskWaker { fn wake(self: Arc) { self.sender.send(self.id); } } // 这是进行实际工作的东西 pub struct Executor { tasks: BTreeMap<u32, Task>, wake_requests: std::sync::mpsc::Receiver<TaskId>, wake_sender: std::sync::mpsc::SyncSender<TaskId> } impl Executor { pub fn new() -> Executor { ... } fn waker_for_task(&self, id: TaskId) -> Waker { Arc::new(TaskWaker { id: task_id, sender: self.wake_sender.clone(); }).into() } // 轮询一个任务,让它按需做一点工作 fn poll_task(&mut self, id: TaskId) { let waker = self.waker_for_task(id); let mut context = Context::from_waker(&waker); // Context 是一个花哨的包装器,用于我们的 waker 类型 if let Poll::Ready(()) = self.tasks[id].poll(context) { self.tasks.remove(id); // 任务已完成!可以移除 } } // 将一个任务放入执行器 pub fn spawn(&mut self, id: TaskId, future: BoxFuture<'static, ()>) { self.tasks.insert(id, future); self.poll_task(id); // 先轮询一次,以便它能注册唤醒回调 } pub fn run(mut self) { // 每次收到唤醒请求,就再次轮询对应的任务 for task_id in self.wake_requests.iter() { self.poll_task(task_id); } } } let mut executor = Executor::new(); executor.spawn(Sleep { registered: false }); executor.run(); ``` ## 好的,但 3DS 呢?我们不是要处理 3DS 吗? 哦对了。那就在下一部分(https://blog.cat-girl.gay/3ds-async-part-two)。

相似文章

Rust异步与ARM通用定时器

Lobsters Hottest

一篇技术博客文章,探讨了在ARM架构上使用ARM通用定时器进行Rust异步编程,比较了定时器外设,并讨论了Embassy和RTIC等框架。

Zig 0.16 中的异步 I/O:今日视角

Lobsters Hottest

Zig 0.16 推出了新的 std.Io 接口,用于跨平台 I/O。zio 库通过栈式协程和操作系统级异步 API 提供了完整的异步实现,无需每个任务一个线程即可实现高效的并发任务。

你的所有智能体都将走向异步

Hacker News Top

文章指出,AI智能体正从同步聊天界面转向异步后台工作流,并重点介绍了Anthropic、OpenAI和Cursor推出的新功能,这些功能将智能体生命周期与HTTP请求-响应周期解耦。