Arc, Weak and reference cycles

Arc is the reference-counted pointer of the previous lesson with an atomic count, so it can cross a thread boundary. Weak is a handle that watches a value without keeping it alive, which is how a structure points back at its parent without a cycle.

This lesson shares a vector with two threads, watches a value through a weak handle, builds a parent and a child that point at each other in one direction only, and ends with the compiler refusing to send an Rc to a thread.

A shared value across threads

Each thread needs its own handle, and a closure that outlives the loop needs to own it, so the clone is made before the spawn and moved into the closure. Arc::clone costs one atomic increment per thread and nothing else.

The program gives a three-element vector to two threads, joins both and adds the lengths. The count is one again after the threads are joined, because both handles were dropped.

use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(vec![1, 2, 3]); let mut handles = Vec::new(); for _ in 0..2 { let shared = Arc::clone(&data); handles.push(thread::spawn(move || shared.len())); } let total: usize = handles.into_iter().map(|handle| handle.join().unwrap()).sum(); println!("{total}"); println!("{}", Arc::strong_count(&data)); }

rustc --edition 2024 arc.rs
./arc

6
1

The move keyword on the closure is what makes the handle travel; without it the closure would borrow a local that does not outlive the thread. The lesson on Rc and RefCell is the single-threaded form of the same type.

Weak and upgrade

Rc::downgrade returns a Weak<T> that does not increment the strong count. Reading through it needs upgrade, which returns Some while the value is alive and None afterwards, so a weak handle can never dangle.

The program takes a weak handle, prints that it can be upgraded, prints both counts, drops the strong handle and prints that the upgrade now fails.

use std::rc::Rc; fn main() { let strong = Rc::new(String::from("data")); let weak = Rc::downgrade(&strong); println!("{}", weak.upgrade().is_some()); println!("{} {}", Rc::strong_count(&strong), Rc::weak_count(&strong)); drop(strong); println!("{}", weak.upgrade().is_none()); }

rustc --edition 2024 weak.rs
./weak

true
1 1
true

The counts are one and one: the strong handle keeps the value alive and the weak handle is counted separately. When the strong count reaches zero the value is dropped, and the weak handle stays valid as an empty handle that reports None.

A cycle and the weak parent

A tree node with a list of children and a pointer to its parent is the standard place for this pair. The children are strong, because a parent owns them, and the parent link is weak, because the child does not own the parent.

The program builds a root and a leaf, links them in both directions, and reads through each link. The weak count of the root is one, and the strong count is one: the leaf does not keep the root alive.

use std::cell::RefCell; use std::rc::{Rc, Weak}; struct Node { name: &'static str, children: RefCell<Vec<Rc<Node>>>, parent: RefCell<Weak<Node>>, } fn main() { let root = Rc::new(Node { name: "root", children: RefCell::new(Vec::new()), parent: RefCell::new(Weak::new()), }); let leaf = Rc::new(Node { name: "leaf", children: RefCell::new(Vec::new()), parent: RefCell::new(Rc::downgrade(&root)), }); root.children.borrow_mut().push(Rc::clone(&leaf)); println!("{}", root.children.borrow()[0].name); println!("{}", leaf.parent.borrow().upgrade().unwrap().name); println!("{} {}", Rc::strong_count(&root), Rc::weak_count(&root)); }

rustc --edition 2024 tree.rs
./tree

leaf
root
1 1

upgrade returns an owned handle, so the temporary in the second print keeps the root alive for the length of the call. A tree that stored the parent as Rc<Node> would compile and would never be freed, which the next section explains.

Why a cycle leaks

Both counts are checked when a handle is dropped. A root that owns a child and a child that owns the root keep each other's count at one, so neither reaches zero, and the two allocations stay alive until the process ends. There is no garbage collector to find them, and the leak is invisible to the compiler.

The fix is a direction: one of the two links is weak. A parent owns its children, and a child points at its parent without owning it. The same rule appears in a doubly linked list, in a subscription that refers back to its publisher, and in a cache that must not keep its keys alive.

Weak is available for both pointers: Rc::downgrade and Arc::downgrade are the same call with the same rule. A weak handle costs one counter word and an upgrade, which is why it is used at the boundary and not in the hot path.

An Rc sent to a thread

Rc updates its count without atomics, so two threads incrementing it would race. The compiler knows this and refuses the spawn: the closure is not Send because it owns a value that is not. The error is E0277 and the note names the missing trait.

The program moves an Rc into a thread. The fix is Arc, which is the same type with an atomic count.

use std::rc::Rc; use std::thread; fn main() { let data = Rc::new(vec![1, 2, 3]); let shared = Rc::clone(&data); thread::spawn(move || { println!("{}", shared.len()); }).join().unwrap(); }

rustc --edition 2024 send_err.rs

error[E0277]: `Rc<Vec<i32>>` cannot be sent between threads safely
--> send_err.rs:7:19
|
7 | thread::spawn(move || {
| ------------- ^------
| | |
| _____|_____________within this `{closure@send_err.rs:7:19: 7:26}`
| | |
| | required by a bound introduced by this call
8 | | println!("{}", shared.len());
9 | | }).join().unwrap();
| |_____^ `Rc<Vec<i32>>` cannot be sent between threads safely
|
= help: within `{closure@send_err.rs:7:19: 7:26}`, the trait `Send` is not implemented for `Rc<Vec<i32>>`
note: required because it's used within this closure
--> send_err.rs:7:19
|
7 | thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/thread/functions.rs:125:0

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

The path with the commit hash is the standard library the compiler was built from, which is how the note points at the bound on spawn. The same error appears for a RefCell shared between threads, for the same reason. The traits behind the message are Send and Sync, and the chapter on concurrency names the types that implement them.

Exercises

  1. Share an Arc<String> with three threads and print its count from the main thread after the joins.
  2. Build a cyclic pair with two Rc fields and then replace one field with a Weak.
  3. Write a function that takes a Weak and returns the length of the value, or zero when it is gone.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does Arc add to Rc?

    Answer

    An atomic count, so it can cross threads

    The atomic increment makes the handle safe to send to another thread.

  2. What does Rc::downgrade return?

    Answer

    A Weak handle that does not own the value

    The weak handle observes the value without changing the strong count.

  3. What does upgrade return when the value is gone?

    Answer

    None

    The weak handle turns into an Option, and None means the strong count reached zero.

  4. Why does a cycle of two strong Rc handles leak?

    Answer

    Each count stays at one, so neither value is dropped

    There is no collector to break the cycle, so one link has to be weak.

  5. What does this program print? use std::rc::Rc; fn main() { let strong = Rc::new(5); let weak = Rc::downgrade(&strong); println!("{}", Rc::weak_count(&strong)); drop(strong); println!("{}", weak.upgrade().is_none()); }

    Answer

    1 / true

    One weak handle exists, and after the strong drop the upgrade fails.

  6. What does this program print? use std::rc::Rc; fn main() { let value = Rc::new(String::from("abc")); let first = Rc::clone(&value); let second = Rc::clone(&value); println!("{}", Rc::strong_count(&second)); drop(first); println!("{}", Rc::strong_count(&value)); }

    Answer

    3 / 2

    Three handles exist, then one is dropped and two remain.

  7. What does this program print? use std::sync::Arc; fn main() { let data = Arc::new(vec![10, 20, 30]); let copy = Arc::clone(&data); println!("{} {}", copy.len(), Arc::strong_count(&data)); }

    Answer

    3 2

    The clone is a second handle on the same three elements.

  8. What does this program print? use std::rc::{Rc, Weak}; fn main() { let value = Rc::new(7); let weak: Weak<i32> = Rc::downgrade(&value); println!("{}", weak.upgrade().map(|inner| *inner + 1).unwrap_or(0)); }

    Answer

    8

    The upgrade succeeds, and the value behind it is seven, so the map adds one.

  9. What is the first line of the compiler output for this program? use std::rc::Rc; use std::thread; fn main() { let data = Rc::new(1); thread::spawn(move || { println!("{}", data); }); }

    Answer

    error[E0277]: Rc<i32> cannot be sent between threads safely

    The closure owns a value that is not Send, so the spawn is refused.

  10. What is the first line of the compiler output for this program? use std::rc::Rc; fn main() { let value = Rc::new(1); let weak = Rc::downgrade(&value); println!("{}", weak); }

    Answer

    error[E0277]: std::rc::Weak<{integer}> doesn't implement std::fmt::Display

    A weak handle has no printable value, so the format macro reports the missing trait.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev