phillip england

[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:

  1. When we have a variable with a trait type which cannot be computer at compile time.
  2. 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.