Rust's Design Principles

comp6991
4 min read
671 words
10 months ago

Throughout my degree, I have picked up on a few key different languages that all have their own unique characteristics and development methodologies. However, after studying COMP6991 which explores the language of Rust, I found it stood out apart from the other languages I have learnt. The focus and concerns that traditionally fall on the developer instead fall on the built-in systems in place that aims to abstract away these problems, making it easier and more efficient to write secure and reliable code.

Originally created by Graydon Hoare (developer/researcher at Mozilla), Rust was aimed at solving a lot of the complex concurrency tasks faced by Mozilla. Developing large-scale systems with concurrent behaviours is extremely difficult; further, maintaining and ensuring safety makes it much more tedious. Rust provided a unique means for handling this problem, and this blog post will explore how Rust achieves fearless concurrency.

The Problem with Concurrency

Computers' ability to execute tasks simultaneously on multiple cores allows for parallel computation. Although providing a huge computational boost to processes, in the context of software development, dealing with these tasks in parallel can lead to race conditions, deadlocks and other concurrency issues if not properly managed. If left unchecked, these issues can cause severe damage, software crashes, and poor performance.

Now Enter... RUST

Rust requires the adoption of idiomatic design choices to guarantee secure access to memory. Here we will explore the key features that Enable Rust to accomplish this:

Ownership System

The ownership system that Rust implements dictates that every value or variable has a single owner, and that owner has exclusive access to that value. Further, when the value goes out of scope, it then loses access, and the value is automatically deallocated; this ensures shared data issues such as data races are unable to occur.

fn main() {
    let x = 5;
    let y = &x;
 
    println!("x: {}", x);
    println!("y: {}", y);
}

In this example we can see that the value x has ownership of the value 5, whereas the value y only has a immutable reference to it. Since y has borrowed an immutable reference it can be safely passed to any thread without the fear of data races'.

Borrowing Mechanism

The borrowing and mutability semantics provided by Rust, enable borrowing of data through mutable or immutable references. Immutable references are threadsafe. Conversely, mutable references are exclusive, meaning only one allowing only one mutable reference to a value at any given time. Below is an example of such semantics.

fn main() {
    let data = vec![1, 2, 3, 4, 5];
 
    // Immutable reference
    let sum = calculate_sum(&data);
    println!("The sum is: {}", sum);
 
    // Mutable reference
    let mut data_copy = data.clone();
    modify_data(&mut data_copy);
    println!("Modified data: {:?}", data_copy);
}
 
// Immutable borrow
fn calculate_sum(data: &[i32]) -> i32 {
    data.iter().sum()
}
 
// Mutable borrow
fn modify_data(data: &mut Vec<i32>) {
    data.push(6);
}
 

Data Sharing

Rust is able to wield marker traits as an identifier of whether values are able to be either Sent or Sync'ed. These traits, more specifically, are called Send and Sync.

Send

The send marker is applied on values that can be safely moved between threads, meaning it possesses no shared mutable state.

The beauty of this trait is that during compile time itself, developers will be able to identify if they are accidentally sending shared mutable data across threads.

Sync

To follow, a type is considered Sync if it is safe to move between threads, indicating concurrent access will not pose issues.

To showcase this, here is a simple snippet that just sends a string value that is then received and printed out.

use std::sync::mpsc;
use std::thread;
 
fn main() {
    let (tx, rx) = mpsc::channel();
    let mut handles = vec![];
 
    for idx in 0..10 {
        let tx_clone = mpsc::Sender::clone(&tx);
        let handle = thread::spawn(move || {
            let value_to_send = format!("Hello from thread {idx}!");
            tx_clone.send(value_to_send).unwrap();
        });
        handles.push(handle);
    }
 
    for message in rx.iter().take(10) {
        println!("Received: {}", message);
    }
 
    for handle in handles {
        handle.join().unwrap();
    }
}