The `arc-swap` library in Rust provides a mechanism for atomically swapping `Arc<T>` pointers, enabling lock-free, highly concurrent reads of shared data structures while allowing occasional updates. It's particularly useful in scenarios where a configuration or a shared state needs to be updated periodically without blocking numerous readers.
Traditionally, shared mutable state in Rust is managed using `Arc<Mutex<T>>` or `Arc<RwLock<T>>`. While these are robust, `Mutex` can introduce contention for *all* accessors (readers and writers), and `RwLock` still involves acquiring locks, even for reads, which can have performance implications under high concurrency or specific scheduling conditions.
`arc-swap` addresses this by using atomic CPU instructions (specifically `compare_exchange` operations on pointers) to replace the underlying `Arc<T>` pointer itself. When a writer wants to update the shared data, it typically creates a *new* `Arc<T>` containing the updated data and then atomically swaps this new `Arc<T>` into the `ArcSwap` instance. Readers, on the other hand, call `load()`, which atomically fetches a *strong reference* (`Arc<T>`) to the currently active data. Once a reader obtains this `Arc<T>`, it holds a valid reference to the data and can access it without any further locks or contention, even if the `ArcSwap` is updated by a writer concurrently.
This pattern is a form of Read-Copy-Update (RCU). The old `Arc<T>` is only dropped once all readers who might have held references to it have finished and released their `Arc` clones. This guarantees that readers always see a consistent snapshot of the data and never encounter partially updated or invalid states.
Key Features and Types:
* `ArcSwap<T>`: The primary type that holds an `Arc<T>`. It allows atomic operations to replace or retrieve the contained `Arc<T>`. The `T` must be `Send + Sync + 'static`.
* `ArcSwapOption<T>`: Similar to `ArcSwap<T>`, but holds an `Option<Arc<T>>`, useful when the shared value might be absent.
* `load()`: Atomically retrieves the current `Arc<T>` from `ArcSwap` and returns a new strong `Arc<T>` clone. This is the main method for readers.
* `store(new_arc)`: Atomically replaces the currently held `Arc<T>` with `new_arc`. The previously held `Arc<T>` is returned and will be dropped when its reference count permits.
* `swap(new_arc)`: Similar to `store`, but returns the *previous* `Arc<T>` directly.
* `compare_and_swap(current_arc, new_arc)`: Atomically updates the stored `Arc<T>` *only if* it currently matches `current_arc`. Useful for conditional updates.
* `rcu(|old_arc| -> Arc<T>)`: A powerful method that takes a closure. The closure receives a reference to the old `Arc<T>`, allowing the user to compute and return a new `Arc<T>` based on it. `rcu` handles retries automatically if another thread updates the value between the read and the swap attempt, ensuring that the update is based on the most recent value.
Advantages:
* Lock-free reads: Readers acquire a reference once and then operate without any further locking, leading to excellent performance in read-heavy scenarios.
* High concurrency: Reduces contention significantly compared to `Mutex` or `RwLock` for shared data where reads are frequent.
* Safe: Leverages Rust's ownership and `Arc`'s reference counting to ensure memory safety and prevent use-after-free issues.
Disadvantages/Considerations:
* Cloning on write: Updates often involve cloning the inner data of the old `Arc<T>` to create a new `Arc<T>` with modifications, which can be more expensive than in-place mutation within a `Mutex`.
* Not for in-place modification: `arc-swap` is designed for replacing entire data structures, not for granular in-place modifications.
* Complexity: Can be slightly more complex to reason about than simple `Mutex` usage.
Example Code
```rust
use arc_swap::ArcSwap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
// Define a configuration struct that we want to share and update.
#[derive(Debug, Clone, PartialEq)]
struct AppConfig {
version: u32,
max_connections: u32,
log_level: String,
}
impl AppConfig {
fn new(version: u32, max_connections: u32, log_level: &str) -> Self {
AppConfig {
version,
max_connections,
log_level: log_level.to_string(),
}
}
}
fn main() {
// 1. Initialize ArcSwap with an initial configuration.
let initial_config = Arc::new(AppConfig::new(1, 100, "INFO"));
let global_config = ArcSwap::new(initial_config);
// Create multiple reader threads
let mut reader_handles = vec![];
for i in 0..3 {
let config_reader = Arc::clone(&global_config);
reader_handles.push(thread::spawn(move || {
for _ in 0..5 {
// Readers use .load() to get a strong Arc reference to the current config.
// This is a lock-free operation.
let current_config = config_reader.load();
println!("[Reader {}] Current config: {{ Version: {}, Max Conn: {}, Log Level: {} }}",
i,
current_config.version,
current_config.max_connections,
current_config.log_level);
thread::sleep(Duration::from_millis(200 + (i * 50) as u64));
}
}));
}
// Create a writer thread to update the configuration periodically
let config_writer = Arc::clone(&global_config);
let writer_handle = thread::spawn(move || {
for v in 2..=4 {
thread::sleep(Duration::from_secs(1));
// Simulate fetching a new config or modifying the old one.
// We must create a *new* Arc<AppConfig> instance.
let new_config = if v % 2 == 0 {
Arc::new(AppConfig::new(v, 100 + v * 10, "DEBUG"))
} else {
Arc::new(AppConfig::new(v, 200 + v * 5, "WARN"))
};
// Writers use .store() to atomically replace the old config with the new one.
// The old Arc is dropped once no more readers hold references to it.
let old_config = config_writer.store(new_config);
println!("[Writer] Updated config to version {}. Old config version was {}.",
v, old_config.version);
}
// Demonstrate rcu() for an update that depends on the previous state.
thread::sleep(Duration::from_secs(1));
config_writer.rcu(|old_config| {
println!("[Writer-RCU] Incrementing version based on old config version {}", old_config.version);
Arc::new(AppConfig::new(
old_config.version + 1,
old_config.max_connections + 50,
&old_config.log_level,
))
});
println!("[Writer-RCU] Config version incremented.");
});
// Wait for all threads to complete
writer_handle.join().expect("Writer thread panicked");
for handle in reader_handles {
handle.join().expect("Reader thread panicked");
}
// Verify the final state (optional)
let final_config = global_config.load();
println!("\n[Main] Final config state: {{ Version: {}, Max Conn: {}, Log Level: {} }}",
final_config.version,
final_config.max_connections,
final_config.log_level);
assert_eq!(final_config.version, 5);
assert_eq!(final_config.log_level, "WARN"); // The last explicit store was WARN, then rcu maintained it.
}
```








arc-swap