489: Arc\<str\> for Shared Strings
Difficulty: 2 Level: Intermediate Atomically reference-counted immutable string โ share one allocation safely across threads.The Problem This Solves
When multiple threads need to read the same string, the options are: clone it into each thread (wastes memory), use `&str` with `'static` (only works for string literals), or reach for synchronisation primitives to guard access (overkill for read-only data). `Arc<str>` is the clean answer. A single heap allocation holds the string bytes and an atomic reference count. Any number of threads can hold a clone of the `Arc<str>`, read it freely, and the allocation lives until the last clone drops โ all without locks. `Arc<str>` is also more compact than `Arc<String>`: it's a fat pointer (address + length) directly to the string bytes, while `Arc<String>` adds an extra level of indirection through a `String` struct.The Intuition
A shared read-only notice board in an office. Every employee (thread) has a laminated copy of the URL to the board (the `Arc`). They can all read it at the same time without asking permission. The board itself is only taken down when every single copy of the URL has been thrown away. No locks, no queuing, just atomic reference counting.How It Works in Rust
1. Create from `&str` or `String`:use std::sync::Arc;
let shared: Arc<str> = "configuration data".into();
let from_string: Arc<str> = some_string.into();
2. Clone across threads โ `Arc<str>` is `Send + Sync`:
let handle = {
let s = shared.clone();
std::thread::spawn(move || println!("thread sees: {}", s))
};
handle.join().unwrap();
3. Use all `str` methods via `Deref`:
shared.contains("config")
shared.split(',').collect::<Vec<_>>()
4. In data structures โ no lifetime parameters needed:
struct Config {
name: Arc<str>,
value: Arc<str>,
}
5. Compare and hash โ `Arc<str>` implements `PartialEq`, `Eq`, `Hash` by comparing string contents (not pointer). For pointer equality use `Arc::ptr_eq`.
What This Unlocks
- Zero-copy sharing across threads โ pass configuration, templates, or parsed strings to worker threads without cloning bytes.
- Smaller than `Arc<String>` โ one less indirection; the string bytes sit directly in the `Arc` allocation.
- Foundation for string interning โ intern tables return `Arc<str>` for thread-safe shared symbols (see example 487).
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Thread-safe shared string | GC-managed | `Arc<str>` (atomic refcount) |
| vs `Rc<str>` | โ | `Rc`: single-thread; `Arc`: multi-thread |
| vs `Arc<String>` | โ | `Arc<str>` is thinner (no extra `String` header) |
| Mutability | Immutable | Immutable (use `Arc<Mutex<String>>` for mutable) |
// 489. Arc<str> for shared strings
use std::sync::Arc;
use std::thread;
fn main() {
// Arc<str> โ shared across threads, O(1) clone
let shared: Arc<str> = Arc::from("shared configuration string");
println!("refcount: {}", Arc::strong_count(&shared));
let handles: Vec<_> = (0..4).map(|id| {
let s = Arc::clone(&shared); // O(1): just increments refcount
thread::spawn(move || {
println!("thread {} sees: '{}'", id, &s[..10]);
s.len() // use the string
})
}).collect();
println!("refcount during threads: {}", Arc::strong_count(&shared));
let results: Vec<usize> = handles.into_iter().map(|h| h.join().unwrap()).collect();
println!("all got len={:?}", results);
println!("refcount after: {}", Arc::strong_count(&shared));
// Arc<str> in a struct (shared across threads)
#[derive(Clone)]
struct Config { name: Arc<str>, value: Arc<str> }
let cfg = Config {
name: Arc::from("host"),
value: Arc::from("localhost"),
};
let handles: Vec<_> = (0..3).map(|_| {
let c = cfg.clone(); // cheap: only increments 2 refcounts
thread::spawn(move || println!("{}={}", c.name, c.value))
}).collect();
for h in handles { h.join().unwrap(); }
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_arc_str_send() {
let s: Arc<str> = Arc::from("hello");
let s2 = Arc::clone(&s);
thread::spawn(move || assert_eq!(&*s2,"hello")).join().unwrap();
}
#[test] fn test_ptr_eq() { let s:Arc<str>=Arc::from("hi"); let c=Arc::clone(&s); assert!(Arc::ptr_eq(&s,&c)); }
#[test] fn test_deref() { let s:Arc<str>=Arc::from("hi"); assert_eq!(s.len(),2); }
}
(* 489. Arc<str> concept โ OCaml *)
(* OCaml GC handles thread-safe sharing; demonstrate with domains *)
let shared_name = "global shared string"
let () =
let domains = Array.init 4 (fun id ->
Domain.spawn (fun () ->
Printf.printf "domain %d: %s (ptr=%d)\n"
id shared_name (Obj.repr shared_name |> Obj.obj)
)
) in
Array.iter Domain.join domains