Phillip England
can anyone understand this language?
12/14/2024
I will be running experiments in my own editor and will be saving the repo
here.
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.
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.
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.
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"] }
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.
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.
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.
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<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:
NOTE TO SELF: Go back and finish "async rust" when you have a better
understanding of traits and lifetimes.