惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Comments on: Blog
S
Schneier on Security
Microsoft Azure Blog
Microsoft Azure Blog
T
Tor Project blog
V
Visual Studio Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
月光博客
月光博客
罗磊的独立博客
Cisco Talos Blog
Cisco Talos Blog
P
Privacy International News Feed
T
Tenable Blog
阮一峰的网络日志
阮一峰的网络日志
AWS News Blog
AWS News Blog
T
ThreatConnect
博客园 - 三生石上(FineUI控件)
Recorded Future
Recorded Future
Hugging Face - Blog
Hugging Face - Blog
T
Tailwind CSS Blog
博客园 - 叶小钗
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
A
Arctic Wolf
L
LINUX DO - 最新话题
美团技术团队
大猫的无限游戏
大猫的无限游戏
I
Intezer
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
量子位
小众软件
小众软件
T
Threatpost
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
宝玉的分享
宝玉的分享
The Register - Security
The Register - Security
Project Zero
Project Zero
J
Java Code Geeks
Cyberwarzone
Cyberwarzone
IT之家
IT之家
MyScale Blog
MyScale Blog
T
Threat Research - Cisco Blogs
T
The Blog of Author Tim Ferriss
腾讯CDC
S
SegmentFault 最新的问题
F
Fox-IT International blog
S
Security Archives - TechRepublic
Last Week in AI
Last Week in AI
G
GRAHAM CLULEY
M
MIT News - Artificial intelligence

Lobsters

Are you a member of any professional associations? readable.css What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 A portentous reunion Accelerating copy_if using SIMD The pressure Just How Bad Was The Intel IAPX432? Common Lisp Portability Library Status JSX.lol ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring The User Is Visibly Frustrated uv must be installed to build a standalone Python distribution Encyclical Letter of His Holiness Leo XIV Magnifica Humanitas (15 May 2026) Using AI to write better code more slowly The Open/Closed Problem in AI A Simple Makefile Tutorial On C extensions, portability, and alternative compilers The social contract of writing Building a Host-Tuned GCC to Make GCC Compile Faster Switching to Colemak | Pedro Alves Fully in-browser container builds Nix's Substituter List Is Not a Routing Table What are you doing this week? Scoped Error in Rust Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The Eternal Sloptember JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting Building Pi With Pi abyss * your_dotfiles_are_not_a_distro Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities From AFSK to Goertzel the entropy layer of a wavelet codec, on its own 10,000 Lines Later: When a Tool Became a Compiler - Rob Durst - Gleam Gathering 2026 Debian SE Linux and PinTheft fht-compositor: A dynamic tiling Wayland compositor A Network Allow-List Won't Stop Exfiltration — André Graf Does bulk memmove speed up std::remove_if? (No.) What is Git made of? wake up! 16b 声明式部分更新 | Blog | Chrome for Developers Don't Roll Your Own ... Dianne Skoll's Web Site - Remind “Long-Term Support” doesn’t mean what you think The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog seriot.ch It's time to talk about my writerdeck hershey Cuneiforth: A Forth for your Chifir z386: An Open-Source 80386 Built Around Original Microcode waylandcraft - Minecraft Mod On the <dl> HP QuickWeb, Singular And Pointless mvm - a fast virtual machine for Go That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian .NET (OK, C#) finally gets union types🎉: Exploring the .NET 11 preview - Part 2 Are we self-sovereign PKI yet? Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public The Soul of Maintaining a New Machine - Third Draft | Books in Progress
Building an AsyncIO executor for the 3DS (pt 1!)
blog.cat-gir · 2026-05-26 · via Lobsters

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?

what?

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.

but the 3ds is cooperative

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...

straight up awaiting it

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.

okay, but how does that work?

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 {}
}

wait! these are just busy loops!

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.

wake me up (wake me up inside)

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();
    }
}

okay, let's do this for real

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();

okay but the 3ds? weren't we working with a 3ds

oh right. that's in the next part