wolfgirl.dev - Async Task Locals From Scratch<br>Async Task Locals From Scratch<br>Published on 2026-06-16 at 09:09:40<br>To make a long story short, let's assume we have the following constraints, ignoring where exactly these constraints come from:<br>We want to associate some data with an async task in Rust, such that, while that task is being polled, we can retrieve said data deeper in the callstack, without having to manually pass the data down thru function arguments.<br>We don't want to use the tokio ecosystem.
That is, we want to do what tokio::task_local! does, but don't want to use tokio. What do? Well... I couldn't find anything else that does it, so I did it myself (:<br>Buckle Up It's Apple-Pie-Baking Time<br>To understand what we want, we must first understand why what the standard library gives is not enough. To understand what the standard library gives, we must first understand the execution model upon which it relies. I did say "from scratch"1, after all :P<br>Most operating systems organize code execution into two distinct units: Processes & Threads . Processes roughly delineate "what memory is visible", while Threads roughly delineate "what instructions are running". A single Process may contain one or more Threads, and when there are multiple Threads in a Process, computer scientists call it a "Multithreaded Architecture" with "Shared Memory Parallelism": there are literally multiple Threads, sharing memory in the same Process.<br>This Process/Thread model requires a bit of work on the programmer's side to make sure Threads don't step on each other's toes while they're running. Most normal code assumes it's the only one with access to the memory it touches, and will get very upset if some other Thread scribbles over its work while it's not looking. If Threads don't touch each other's memory, or there is some mechanism by which they co-ordinate their mutual touching2, that property is called "Thread-Safety".<br>Naturally, thread-safety is a very important property (ensuring it at compile time is a big motivator for Rust!), so we'll need to maintain it while achieving our overall goal. There are various strategies by which we can ensure thread-safety, but by far the simplest is "just use variables on the stack lol". Each Thread's stack is unique by construction3, so if we create a value on the stack + pass a reference down thru arguments to anywhere that needs it, boom done ezpz.<br>But remember, we have a special requirement:<br>[...] can retrieve said data deeper in the callstack, without having to manually pass the data down thru arguments .
So really, we want something that works more like a "global" variable, but without the complications that come from multiple Threads sharing a single global. Fortunately, this already exists! Thread-Local Storage , aka TLS, creates variables that are "local" in the sense they can only be accessed by a single thread, while still being "global" in the sense that any function can touch them. Different platforms can have greatly varying implementations of TLS, all we need to know is: each thread has its own place to put its global variables, no more toe-stepping yay everyone is happy. C/C++ uses the thread_local keyword for this, while Rust uses a std::thread_local! macro.<br>Are Thread-Locals Sufficient?<br>Turns out no! There's a bit more universe we need to conjure before we can start on our pie, specifically the difference between an Operating System Thread and a Rust Task.<br>With Threads, the OS automatically manages things like "what Thread is running on this CPU core?", "what Threads are available to run but aren't?", & "ok time to switch to different Thread". However, switching between Threads at the OS level is quite a slow operation4 compared to normal code. Instead, languages will often make their own thread-like things in "userspace" (that is, without any help from the OS) to help avoid this slowdow while keeping the other benefits of concurrent code. These thread-like things are called either Green Threads or Coroutines, depending on certain factors.<br>Aside on those factors The main factor is "can it run on its own" (Green Thread, aka pre-emptive multi-tasking) or "does it need something else to drive it" (Coroutine, aka co-operative multi-tasking). Common examples of Green Threads are Erlang's processes & Go's goroutines (yeah they really messed up the name there), while examples of Coroutines include Python's Coroutines, C++'s Coroutines, & Rust's Future & Iterator traits. Javascript's Promise objects are interesting, because at first glance they seem to be a Coroutine, what with the async/await syntax, but actually they're Green Threads because they start running all on their own, and the await syntax is just a fancy way of chaining callbacks together.<br>Anyways!! These distinctions tend to be pretty controversial, and I'm sure I'll get some helpful comments clarifying how I got some of this wrong (:<br>For now, I'll just group them all together under an umbrella labeled "Tasks",...