avatarJohn Philip

Summarize

Rustlings: arc1.rs #Issue80 —Smart Pointers in Rust

Rustlings Challenge: arc1.rs Solution Walkthrough

Image by João Henrique Machado Silva

This is the Eightieth (80th) issue of the Rustlings series. In this issue, we provide solutions to Rustlings exercises along with detailed explanations. In this issue we will solve the challenge on arc1.rs.

Previous challenge #Issue 79

Smart Pointers Introduction

Smart pointers in Rust are a category of data structures that provide additional functionality and safety guarantees beyond regular pointers. They “smartly” manage memory and ownership, ensuring memory safety and helping prevent common programming errors like null pointer dereferencing and memory leaks.

Smart pointers are a key feature of Rust’s ownership system and play a crucial role in enabling memory-safe systems programming.

The primary smart pointers in Rust are:

  1. Box: This is the simplest smart pointer and represents an owned heap-allocated value. It allows you to allocate memory on the heap and store values there. It ensures that when the Box goes out of scope, the memory it owns is deallocated automatically, preventing memory leaks. It’s commonly used for creating recursive data structures or when you need to have a known-sized type with a fixed lifetime.
  2. Rc (Reference Counting): Rc stands for “reference counting,” and it enables multiple ownership of the same data. It keeps track of how many references exist to a value and deallocates the value when the last reference is dropped. This is useful when you need to share data across different parts of your program but still want to ensure memory safety.
  3. Arc (Atomic Reference Counting): Arc is similar to Rc, but it adds atomic operations to ensure thread safety. It’s used when you need to share data across multiple threads.
  4. Mutex and RwLock: These smart pointers are used for managing concurrent access to data. Mutex enforces exclusive access, while RwLock allows multiple readers or one writer at a time. They are often used in multi-threaded programs to synchronize access to shared resources.
  5. Cell and RefCell: These are smart pointers used for interior mutability, allowing you to change the value inside an immutable reference. Cell is for single-threaded scenarios, while RefCell is used for multi-threaded scenarios and enforces runtime borrowing rules.

Smart pointers in Rust solve several problems:

  1. Memory Safety: Smart pointers help prevent common memory-related bugs like null pointer dereferencing, dangling pointers, and memory leaks.
  2. Ownership Management: They allow you to express and manage ownership relationships between data structures, ensuring that data is cleaned up properly when it’s no longer needed.
  3. Sharing Data: Smart pointers like Rc and Arc enable safe and efficient data sharing among different parts of a program or between threads.
  4. Interior Mutability: Smart pointers like Cell and RefCell provide a safe way to mutate data inside an immutable reference, which is essential in some cases, such as updating cached values.
  5. Thread Safety: Arc and synchronization primitives like Mutex and RwLock help you write concurrent programs with confidence by ensuring thread safety and preventing data races.

Challenge:

// arc1.rs
//
// In this exercise, we are given a Vec of u32 called "numbers" with values
// ranging from 0 to 99 -- [ 0, 1, 2, ..., 98, 99 ] We would like to use this
// set of numbers within 8 different threads simultaneously. Each thread is
// going to get the sum of every eighth value, with an offset.
//
// The first thread (offset 0), will sum 0, 8, 16, ...
// The second thread (offset 1), will sum 1, 9, 17, ...
// The third thread (offset 2), will sum 2, 10, 18, ...
// ...
// The eighth thread (offset 7), will sum 7, 15, 23, ...
//
// Because we are using threads, our values need to be thread-safe.  Therefore,
// we are using Arc.  We need to make a change in each of the two TODOs.
//
// Make this code compile by filling in a value for `shared_numbers` where the
// first TODO comment is, and create an initial binding for `child_numbers`
// where the second TODO comment is. Try not to create any copies of the
// `numbers` Vec!
//
// Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

#![forbid(unused_imports)] // Do not change this, (or the next) line.
use std::sync::Arc;
use std::thread;

fn main() {
    let numbers: Vec<_> = (0..100u32).collect();
    let shared_numbers = // TODO
    let mut joinhandles = Vec::new();

    for offset in 0..8 {
        let child_numbers = // TODO
        joinhandles.push(thread::spawn(move || {
            let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum();
            println!("Sum of offset {} is {}", offset, sum);
        }));
    }
    for handle in joinhandles.into_iter() {
        handle.join().unwrap();
    }
}

Explanation:

To make the code compile and ensure that it’s thread-safe, we can use an Arc (Atomic Reference Counter) for the numbers Vec. Additionally, we can clone this Arc for each thread to ensure that they all have their own reference to the shared data.

Solution:

#![forbid(unused_imports)] // Do not change this, (or the next) line.
use std::sync::Arc;
use std::thread;

fn main() {
    let numbers: Vec<_> = (0..100u32).collect();
    let shared_numbers = // TODO
    let mut joinhandles = Vec::new();

    for offset in 0..8 {
        let child_numbers = // TODO
        joinhandles.push(thread::spawn(move || {
            let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum();
            println!("Sum of offset {} is {}", offset, sum);
        }));
    }
    for handle in joinhandles.into_iter() {
        handle.join().unwrap();
    }
}

In the code:

  • We use Arc::new(numbers) to create an Arc that wraps the numbers Vec, making it thread-safe.
  • Within the loop, we clone the Arc for each thread using Arc::clone(&shared_numbers) to ensure that each thread has its own reference to the shared data.
  • We use a Mutex for each thread’s access to the numbers Vec. In this case, since the Arc contains an immutable reference to the Vec, we don’t need to lock the Mutex for read access.
  • The threads use enumerate() to iterate over the elements of the child_numbers Vec, and the filter() and map() methods are used to calculate the sum for each offset.

You can experiment with the code on Rust Playground.

Resources

Before you go

Thank you for taking the time to read through this challenge. We invite you to share your knowledge of Rust as well. If you found this article valuable, please don’t hesitate to share it with others. Don’t forget to follow the publication and give the article some claps 👏.

Thank you, and we look forward to seeing you for the next challenges!

More reads

Rust
Rustlang
Rust Programming Language
Rustling
Programming
Recommended from ReadMedium