Sling Academy
Home/Rust/Rust - Safely Sharing Ownership with Rc and Arc in Struct Fields

Rust - Safely Sharing Ownership with Rc and Arc in Struct Fields

Last updated: January 03, 2025

In Rust, memory safety and concurrency are at the forefront of its design philosophy. One of the language's features, safe shared ownership, is achieved through reference counting mechanisms provided by Rc and Arc. These utilities are invaluable when struct fields need to be shared among multiple parts of an application without relinquishing the benefits of Rust’s ownership system.

Understanding Rc and Arc

Rc, short for Reference Counted, is a single-threaded reference counting mechanism for Rust applications. It enables multiple ownership of data, meaning that several variables can own a value at the same time. The value is dropped when the last owning variable goes out of scope.


use std::rc::Rc;

fn main() {
    let a = Rc::new(5);
    let b = a.clone();
    let c = Rc::clone(&a);
    println!("Value of a: {}\nValue of b: {}\nValue of c: {}", a, b, c);
}

In this example, the variable a owns the data initially. By cloning a into b and c, all of them now share ownership, illustrated through reference counting.

Introducing Thread Safety with Arc

When working in a multi-threaded context, Arc, which stands for Atomic Reference Counted, should be used instead of Rc, as it is thread-safe. Arc allows safe sharing across thread boundaries by utilizing atomic operations.


use std::sync::Arc;
use std::thread;

fn main() {
    let a = Arc::new(5);
    let mut handles = vec![];

    for _ in 0..10 {
        let a = Arc::clone(&a);
        let handle = thread::spawn(move || {
            println!("Value: {}", a);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

In the above program, Arc::clone(&a) is used to share the ownership among multiple threads safely. Each thread prints the value, which they access concurrently without risking data races.

Using Rc or Arc in Structs

Structs in Rust benefit from Rc and Arc particularly when the aim is to share mutable ownership or make them available across a complex set of references safely. Let’s consider an example with Arc:


use std::sync::Arc;

struct SharedData {
    contents: Arc,
}

fn main() {
    let contents = Arc::new(String::from("Hello, world!"));
    let struct1 = SharedData {
        contents: Arc::clone(&contents),
    };

    let struct2 = SharedData {
        contents: Arc::clone(&contents),
    };

    println!("Struct1 contents: {}", struct1.contents);
    println!("Struct2 contents: {}", struct2.contents);
}

In this example, Arc is used within a struct to allow multiple structs to own the same data safely, reflecting shared memory scenarios in concurrent programming.

Combining RefCell with Rc

In situations where Rc is required but interior mutability is also desired, a pattern involving RefCell can be employed. RefCell is useful when the data should be mutable from the owner’s perspective.


use std::cell::RefCell;
use std::rc::Rc;

struct Owner {
    name: String,
}

struct Gadget {
    id: i32,
    owner: Rc>,
}

fn main() {
    let owner = Rc::new(RefCell::new(Owner { name: String::from("Gadgetman") }));

    let gadget1 = Gadget { id: 1, owner: Rc::clone(&owner) };
    let gadget2 = Gadget { id: 2, owner: Rc::clone(&owner) };

    owner.borrow_mut().name = String::from("Changed" );

    println!("Gadget 1 Owner: {}", gadget1.owner.borrow().name);
    println!("Gadget 2 Owner: {}", gadget2.owner.borrow().name);
}

With RefCell, data can be borrowed as mutable during runtime, although it's wrapped in an Rc in this context. It’s crucial to use this pattern with understanding, as it introduces runtime borrowing checks.

Conclusion

Utilizing Rc and Arc strategically can significantly enhance Rust applications by allowing safe sharing of data, both in single-threaded and multi-threaded contexts. These tools maintain Rust's promise of safety without incurring the dangers traditionally associated with shared mutable states. Understanding and leveraging these constructs can unlock optimized design patterns in your Rust applications.

Next Article: Working with HashMap and Other Collections Inside Rust Structs

Previous Article: Combining Enums and Structs for Rich Data Models in Rust

Series: Working with structs in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior