[async rust]
written 12/14/2024
can anyone understand this language?
Article Sandbox
I will be running experiments in my own editor and will be saving the repo here.
My Experience With Rust
Rust is going to be my language for 2025. I've tinkered with a low-level networking framework called Zeke and I would like to continue working on that project.
Since working on Zeke, I've learned a bit more about programming on an interface level, and I think those skills will help a lot.
Rust has a few concepts that are challenging to me such as lifetimes and the borrow checker. But the number one thing which caused me challenges when writing zeke was multi-threaded rust.
If I had a good grapse on those concepts I would feel way more comfortable in the language. I think before I start diving into all the different multi-threaded types rust provides, I need to understand it's async model better.
Let's Get Rusty
Any time I think about rust, I think about Bodgen from Let's Get Rusty on youtube.
This video is where I am going to start.
Futures
The first big takeaway is that async
functions in rust are just a facade over a function which returns a Future
.
1async fn my_function() {
2 println!("I'm an async function!")
3}
is really just a fasade over:
1fn my_function() -> impl Future<Output = ()> {
2 println!("I'm an async function too!")
3}
I visited the docs and it looks like Future
is a trait which can be polled
to completetion.
When we use the await
keyword, we are using a fasade over the poll
method which is associated with the Future
trait.
Tokio
Bodgen explains that Futures
must be manually polled
to completetion, which is cumbersom. But, that is why a runtime like tokio exists.
In a language like Javascript, Promises
are handled by the language underneath the hood. But in Rust, the async
runtime is not included in the std lib
, so options like tokio
have emerged.
I went ahead and added tokio
to my cargo.toml
:
1[package]
2name = "sandbox-async-rust"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7tokio = { version = "1", features = ["full"] }
Tokio Tasks
Tasks are used to make our code run concurrently. Tasks a green threads and are non-blocking similar to gorountines.
I discovered that tokio
attempts to mimic the api provided by the Rust std lib
for traditional threads. This makes it easy to swap between using tasks and traditional threads without a paradigm shift.
Futures Are Lazy
This is something to be noted. Futures
are lazy in Rust which means we can collect our tasks and then call await
on them later. If we do not await
a task, then we do not experience any runtime cost for the task.
This is different than other languages that use the async/await
syntax to handle asyncronous code.
Morphing Data Across Threads
After a bit of playing around, I found myself wondering how to make changes to data across multiple threads.
I ended up with something like this:
1async fn morph_data_across_threads() {
2 let str = String::from("I will be morphed!");
3 let str_arc = Arc::new(Mutex::new(str));
4 let mut handles = vec![];
5 for i in 0..10 {
6 let str_clone = Arc::clone(&str_arc);
7 let task = tokio::spawn(async move {
8 // lock the mutex to modify it
9 let mut val = str_clone.lock().await;
10 val.push_str(&i.to_string());
11 });
12 handles.push(task);
13 }
14 for task in handles {
15 task.await.unwrap();
16 }
17 // retrieve the final value as str is expended
18 let final_str = str_arc.lock().await;
19 println!("{}", final_str);
20}
And with this we see the introduction of a few core types I think I'll need to study. I see Arc
and Mutex
. We are calling lock()
and these are concepts I think I'll need to get a better grasp on.
The code runs, and I have a general understanding as to what is going on under the hood, but from what I understand about Rust, the way these types of constructs impact memory is important to get right.
Smart Pointers
After I started researching, I came across this video on smart pointers.
ChatGPT says: "A smart pointer is an object that acts like a pointer but provides additional features to manage the ownership, lifecycle, and memory of dynamically allocated resources. It is typically used in programming languages like C++ and Rust to handle memory safely and efficiently."
So, it looks like these constructs are called smart pointers. I am going to dig through them and do my best to get a surface level understanding of them.
Box
Box<T>
enables us to dictate that some data should be stored on the heap instead of the stack.
We store things on the heap when we have no way of knowing the size of the data at compile time. You want to try and avoid storing things on the heap, but in certain situations it cannot be avoided.
The video points out Box<T>
has 2 use-cases:
- When we have a variable with a trait type which cannot be computer at compile time.
- When we have a recursive data type whos fields include the struct it is derived from.
NOTE TO SELF: Go back and finish "async rust" when you have a better understanding of traits and lifetimes.