2.7 KiB
+++ title = "A Mental Model for Async Rust" date = 2022-07-29 draft = true
[taxonomies] tags = ["rust", "async"] +++
When I tried to do async Rust, I got a bunch of errors from the borrow checker that, to me, it didn't make sense -- and wouldn't be an issue if I was using threads.
It took me awhile to figure out a mental model for doing it right.
A problem with naming
I think my initial problem started with naming. The concept of async/await is quite recent, but for a long time we've been talking about "greenthreads" and "light-weight threads" -- "threads" that are managed by the application and not the OS. While there are some differences between greenthreads and async things, the naming stuck with me (and I think I saw some posts linking the two).
Still on naming, Tokio, the most popular async framework
in Rust, uses task::spawn
to spawn a new task, which is pretty close to the
thread call, thread::spawn
-- and both return a structure called
JoinHandle
-- so this mixture of "tasks/greenthreads are threads" got pretty
ingrained to me.
{% note() %} Yeah, yeah, other languages avoid this by using their own words, but my contact with async was with Rust, so... {% end %}
A problem with structure
So you get this "async is thread" mentality due aproximation. And then you try to build something async using the same model.
For example, a producer/consumer in Rust would be something like:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
let self_tx = tx.clone();
let consumer = thread::spawn(move || {
while let Ok(msg) = rx.recv() {
println!("Message: {}", msg);
if msg > 1000 {
// actually, we just need to drop self_tx, otherwise the consumer will keep waiting
// for inputs from it, even when tx was already dropped when the producer ended.
// the problem with a direct drop is that rustc can't see that it won't be used
// anymore.
break;
} else if msg % 2 == 0 {
if self_tx.send(msg * 2).is_err() {
println!("Failed to push new value to consumer");
break;
};
}
}
});
let producer = thread::spawn(move || {
for i in 1..12 {
if tx.send(i).is_err() {
println!("Failed to send {}, ending producer", i);
break;
}
}
// tx.send(0);
});
producer.join().unwrap();
consumer.join().unwrap();
}
(Yeah, I did all in a single file. Sue me.)
[async is channel, not spawn]