




















the Nintendo 3DS is a very fun device to write homebrew for, but one thing about it can be annoying: its multitasking is non-preemptive!
in this little series, we'll be building an asyncio executor for the 3ds. in this first part: why and what is async, anyways?
the way threads and processes usually work in operating systems is through pre-emption: a thread can only run for a little bit of time before it gets temporarily paused so the next thread in line can run.
so, if you spawn a super intensive long-running thread, it won't be able to hog the entire CPU, as the OS will take over and make sure everyone else can get some CPU time.
this means that threads are only paused when they ask to be. so if our super intensive long-running thread never gives up the CPU, nothing else will be able to run!
of course, we can just be careful and make sure it doesn't, but it's really easy to forget. it'd be convenient if there was a way to model this problem in the way we write our code...
async programming is just such a model! in an async program, we model each bit of work as a task. a task is not supposed to hog the CPU: it should do a little bit of work, then go to sleep while waiting for something to happen (e.g, a TCP packet to come in), then do a little more work. a thing we can wait on is called a Future.
none of this is impossible to do without async: it just makes it very explicit. this is specially important in the case of the 3ds, where platform behaviour is... sometimes unpredictable in terms of what will cause a task to yield or not.
// in a normal program, it can be hard to know whether this will put our thread to sleep or not... socket.read(); // but in async, it's explicit that it will make our task wait! socket.read().await;
the magic .await indicates that we want our task to sleep until socket.read() (which is a Future) is complete.
i must confess: sleep is a misnomer for what's happening here. internally, a task isn't like a thread at all: it's a function we call over and over, like this
while task.do_some_work() == NotReady {}
so if we have a task that reads from a socket twice, like:
let task = async { socket.read().await; socket.read().await; }
it becomes something like this:
let task = { let mut read_operation = socket.read(); while read_operation() == NotReady {} let mut read_operation_two = socket.read(); while read_operation() == NotReady {} }
correct: this async sucks, because it actually isn't giving up the CPU at all! it might, in fact, be taking up more of it, as it calls the same function over and over in a loop. instead, we only want to call our do_some_work() or read_operation() function when new work can actually be done (e.g, data can be read from a socket). this might sound familiar if you've ever worked with callbacks.
socket.on_has_data(|| { read(); });
callbacks, however, quickly descend into hell, and were actually invented by the indentation industry to sell more TAB keys.
instead, rust's async system uses Wakers and Executors.
an Executor is in charge of our tasks: it's what calls the do_some_work() function. but it will not do that unless we explicitly ask it to. a Waker is an abstraction over "asking the executor to please call our function again, pretty please".
fn do_some_work() { // if our data is ready, we can just read it and return immediately! if socket.has_data() { return Ready(socket.read()) } else { // else, we register ourselves with the socket: it will tell the executor to call us again when it actually has some data. socket.on_has_data(|| executor.wake_me_up()); return NotReady // we aren't ready, so we return for now! } } fn executor() { if do_some_work() == Ready(data) { return data }; // we call it once, just in case it has data already, or so that it can register itself for waking later. while let Some(wake_request) = wake_requests.receive() { // we can use a channel or something like it for receiving wake requests do_some_work(); } }
all the examples so far have been very pseudo-code-y. let's actually try to build a minimum viable executor, alongside a bad minimalist implementation of sleep().
pub struct Sleep { registered: bool } impl Future for Sleep { type Output = (); // instead of our Ready/NotReady pseudocode, here we use the standard library's Poll<T> type, which has two variants: Ready(val) or Pending. fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { if !self.registered { // this is our first time being called! let waker = context.waker().clone(); std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(5)); waker.wake(); // ask the executor to call the Sleep future again }); self.registered = true; // make sure we know we've already asked to be woken up in five seconds return Poll::Pending // we aren't done yet! } else { // this isn't our first time being called, so we must have been woken up by our waker! return Poll::Ready(()) } } } // lets just use some ints to identify our tasks! pub struct TaskId(u32); // for reasons that i will not get into here, futures have some very specific memory placement requirements which make it easier to just put our tasks on the heap. here we're using the `futures`' crate BoxFuture type alias. pub type Task = BoxFuture<'static, ()>; // we're just going to use an mpsc channel for our wake-up requests, so our `Waker` is just a channel sender + the id we'd like to wake up pub struct TaskWaker { id: TaskId, sender: std::sync::mpsc::SyncSender<TaskId> } // we need to implement the Wake trait to make Wakers: it just defines exactly how we go about waking up a task in our setup impl Wake for TaskWaker { fn wake(self: Arc<Self>) { self.sender.send(self.id); } } // this is what will do the heavy lifting pub struct Executor { tasks: BTreeMap<TaskId, BoxFuture<'static, ()>>>, 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() } // poll a task, letting it do some work as it wishes fn poll_task(&mut self, id: TaskId) { let waker = self.waker_for_task(task_id); let mut context = Context::from_waker(&waker); // a Context is a fancy little wrapper for our waker type if let Poll::Ready(()) = self.tasks[task_id].poll(context) { self.tasks.remove(task_id); // task is done! we can remove it } } // place a task on our executor pub fn spawn(&mut self, id: TaskId, future: BoxFuture<'static, ()>) { self.tasks.insert(id, future); self.poll_task(id); // we give it an initial poll so it can set up its waker registration } pub fn run(mut self) { // every time we get a wake request, we poll the task it tells us to again 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();
oh right. that's in the next part
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。