Rust - Weak References
Overview
Estimated time: 45–60 minutes
Master weak references in Rust to break reference cycles and manage memory efficiently. Learn when and how to use Weak<T>
with Rc
and Arc
for preventing memory leaks in complex data structures.
Learning Objectives
- Understand the problem of reference cycles
- Use Weak<T> to break cycles in Rc structures
- Apply weak references in tree and graph data structures
- Manage memory efficiently with weak references
Prerequisites
The Reference Cycle Problem
Reference cycles can cause memory leaks when using Rc<T>
:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell
Expected output:
Creating reference cycle...
Parent strong count: 2
Child strong count: 2
Function ended - nodes may not be dropped due to cycle!
Program ending...
Breaking Cycles with Weak References
Use Weak<T>
to break reference cycles:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell
Expected output:
Creating tree with weak references...
Parent strong count: 1
Parent weak count: 1
Child strong count: 2
Child's parent value: 1
Function ended - nodes should be properly dropped!
Dropping node with value 2
Dropping node with value 1
Program ending...
Tree Data Structure with Weak References
Implement a proper tree using weak references for parent pointers:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
type TreeNode = Rc>;
type WeakNode = Weak>;
#[derive(Debug)]
struct Node {
value: String,
parent: Option,
children: Vec,
}
impl Node {
fn new(value: &str) -> TreeNode {
Rc::new(RefCell::new(Node {
value: value.to_string(),
parent: None,
children: Vec::new(),
}))
}
fn add_child(parent: &TreeNode, child: TreeNode) {
child.borrow_mut().parent = Some(Rc::downgrade(parent));
parent.borrow_mut().children.push(child);
}
fn print_tree(node: &TreeNode, indent: usize) {
let node_ref = node.borrow();
println!("{:indent$}{}", "", node_ref.value, indent = indent);
for child in &node_ref.children {
Self::print_tree(child, indent + 2);
}
}
fn get_parent_value(node: &TreeNode) -> Option {
let node_ref = node.borrow();
node_ref.parent.as_ref()
.and_then(|weak_parent| weak_parent.upgrade())
.map(|parent| parent.borrow().value.clone())
}
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping node: {}", self.value);
}
}
fn main() {
let root = Node::new("Root");
let child1 = Node::new("Child 1");
let child2 = Node::new("Child 2");
let grandchild = Node::new("Grandchild");
// Build the tree
Node::add_child(&root, child1.clone());
Node::add_child(&root, child2.clone());
Node::add_child(&child1, grandchild.clone());
println!("Tree structure:");
Node::print_tree(&root, 0);
// Access parent information
println!("\nParent relationships:");
if let Some(parent) = Node::get_parent_value(&child1) {
println!("Child 1's parent: {}", parent);
}
if let Some(parent) = Node::get_parent_value(&grandchild) {
println!("Grandchild's parent: {}", parent);
}
println!("\nReference counts:");
println!("Root strong: {}, weak: {}",
Rc::strong_count(&root), Rc::weak_count(&root));
println!("Child1 strong: {}, weak: {}",
Rc::strong_count(&child1), Rc::weak_count(&child1));
}
Expected output:
Tree structure:
Root
Child 1
Grandchild
Child 2
Parent relationships:
Child 1's parent: Root
Grandchild's parent: Child 1
Reference counts:
Root strong: 1, weak: 2
Child1 strong: 2, weak: 1
Dropping node: Root
Dropping node: Child 1
Dropping node: Grandchild
Dropping node: Child 2
Observer Pattern with Weak References
Implement an observer pattern using weak references:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
trait Observer {
fn notify(&self, message: &str);
}
struct Subject {
observers: RefCell>>,
}
impl Subject {
fn new() -> Self {
Subject {
observers: RefCell::new(Vec::new()),
}
}
fn add_observer(&self, observer: Weak) {
self.observers.borrow_mut().push(observer);
}
fn notify_all(&self, message: &str) {
// Clean up dead weak references and notify living ones
self.observers.borrow_mut().retain(|weak_observer| {
if let Some(observer) = weak_observer.upgrade() {
observer.notify(message);
true // Keep this observer
} else {
false // Remove dead weak reference
}
});
}
fn observer_count(&self) -> usize {
self.observers.borrow().len()
}
}
struct ConcreteObserver {
name: String,
}
impl ConcreteObserver {
fn new(name: &str) -> Rc {
Rc::new(ConcreteObserver {
name: name.to_string(),
})
}
}
impl Observer for ConcreteObserver {
fn notify(&self, message: &str) {
println!("{} received: {}", self.name, message);
}
}
impl Drop for ConcreteObserver {
fn drop(&mut self) {
println!("Observer {} is being dropped", self.name);
}
}
fn main() {
let subject = Subject::new();
// Create observers
let observer1 = ConcreteObserver::new("Observer1");
let observer2 = ConcreteObserver::new("Observer2");
let observer3 = ConcreteObserver::new("Observer3");
// Add observers using weak references
subject.add_observer(Rc::downgrade(&observer1));
subject.add_observer(Rc::downgrade(&observer2));
subject.add_observer(Rc::downgrade(&observer3));
println!("Initial observer count: {}", subject.observer_count());
// Notify all observers
subject.notify_all("First message");
// Drop one observer
drop(observer2);
println!("\nAfter dropping observer2:");
// This will clean up the dead weak reference
subject.notify_all("Second message");
println!("Observer count after cleanup: {}", subject.observer_count());
// Drop another observer
drop(observer1);
println!("\nAfter dropping observer1:");
subject.notify_all("Third message");
println!("Final observer count: {}", subject.observer_count());
}
Expected output:
Initial observer count: 3
Observer1 received: First message
Observer2 received: First message
Observer3 received: First message
Observer Observer2 is being dropped
After dropping observer2:
Observer1 received: Second message
Observer3 received: Second message
Observer count after cleanup: 2
Observer Observer1 is being dropped
After dropping observer1:
Observer3 received: Third message
Final observer count: 1
Observer Observer3 is being dropped
Weak References with Arc (Thread-Safe)
Use weak references in multi-threaded contexts:
use std::sync::{Arc, Weak, Mutex};
use std::thread;
use std::time::Duration;
struct SharedResource {
id: usize,
data: Mutex,
}
impl SharedResource {
fn new(id: usize, data: &str) -> Arc {
Arc::new(SharedResource {
id,
data: Mutex::new(data.to_string()),
})
}
fn update_data(&self, new_data: &str) {
let mut data = self.data.lock().unwrap();
*data = new_data.to_string();
println!("Resource {} updated to: {}", self.id, data);
}
fn get_data(&self) -> String {
self.data.lock().unwrap().clone()
}
}
impl Drop for SharedResource {
fn drop(&mut self) {
println!("Dropping resource {}", self.id);
}
}
fn worker_thread(weak_resource: Weak, worker_id: usize) {
for i in 0..5 {
thread::sleep(Duration::from_millis(100));
// Try to upgrade the weak reference
if let Some(resource) = weak_resource.upgrade() {
let data = resource.get_data();
println!("Worker {} (iteration {}): Resource data = '{}'",
worker_id, i, data);
} else {
println!("Worker {}: Resource has been dropped", worker_id);
break;
}
}
}
fn main() {
let resource = SharedResource::new(1, "Initial data");
// Create weak references for worker threads
let weak1 = Arc::downgrade(&resource);
let weak2 = Arc::downgrade(&resource);
// Spawn worker threads with weak references
let handle1 = thread::spawn(move || worker_thread(weak1, 1));
let handle2 = thread::spawn(move || worker_thread(weak2, 2));
// Update resource data
thread::sleep(Duration::from_millis(250));
resource.update_data("Updated data");
// Drop the main resource after a delay
thread::sleep(Duration::from_millis(200));
println!("Main thread dropping resource...");
drop(resource);
// Wait for worker threads to complete
handle1.join().unwrap();
handle2.join().unwrap();
println!("All threads completed");
}
Expected output:
Worker 1 (iteration 0): Resource data = 'Initial data'
Worker 2 (iteration 0): Resource data = 'Initial data'
Worker 1 (iteration 1): Resource data = 'Initial data'
Worker 2 (iteration 1): Resource data = 'Initial data'
Resource 1 updated to: Updated data
Worker 1 (iteration 2): Resource data = 'Updated data'
Worker 2 (iteration 2): Resource data = 'Updated data'
Main thread dropping resource...
Dropping resource 1
Worker 1: Resource has been dropped
Worker 2: Resource has been dropped
All threads completed
Best Practices
1. When to Use Weak References
use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Good: Parent-child relationships in trees
struct TreeNode {
parent: Option>, // Weak to parent
children: Vec>, // Strong to children
}
// Good: Observer pattern
struct Subject {
observers: RefCell>>,
}
// Good: Cache-like structures
struct Cache {
items: RefCell>>,
}
fn main() {
println!("Use weak references for:");
println!("1. Breaking reference cycles");
println!("2. Parent pointers in trees");
println!("3. Observer patterns");
println!("4. Cache implementations");
println!("5. Callback registrations");
}
2. Always Check if Weak References are Still Valid
use std::rc::{Rc, Weak};
fn safe_access(weak_ref: &Weak) -> Option {
// Always use upgrade() and handle the None case
weak_ref.upgrade().map(|strong| strong.clone())
}
fn unsafe_access(weak_ref: &Weak) -> String {
// Don't do this - it will panic if the reference is dead
// weak_ref.upgrade().unwrap().clone()
// Do this instead
weak_ref.upgrade()
.map(|strong| strong.clone())
.unwrap_or_else(|| "Reference is dead".to_string())
}
fn main() {
let strong = Rc::new("Hello".to_string());
let weak = Rc::downgrade(&strong);
println!("Before drop: {:?}", safe_access(&weak));
drop(strong);
println!("After drop: {:?}", safe_access(&weak));
println!("Safe fallback: {}", unsafe_access(&weak));
}
Checks for Understanding
Question 1: What happens when you try to upgrade()
a dead weak reference?
Answer: upgrade()
returns None
when the referenced object has been dropped. Always handle this case to avoid panics.
Question 2: Why use weak references for parent pointers in tree structures?
Answer: Using strong references for both parent and child relationships creates reference cycles, preventing nodes from being dropped. Weak references to parents break the cycle while maintaining the relationship.
Question 3: Can you have multiple weak references to the same object?
Answer: Yes, you can create multiple weak references to the same object. Each weak reference can be upgraded independently, and the object is only dropped when all strong references are gone.