309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
//! ============================================================================
|
||
//! LEARNING GUIDE - PART 4: Efficient Broadcasting with Arc
|
||
//! ============================================================================
|
||
//!
|
||
//! WHAT YOU'LL LEARN:
|
||
//! - Why Arc<T> is important for efficient broadcasting
|
||
//! - How to avoid expensive clones with large data
|
||
//! - Memory efficiency with Arc reference counting
|
||
//! - When to use Arc<T> vs direct values
|
||
//!
|
||
//! PREREQUISITES:
|
||
//! - Read examples 01, 02, and 03 first
|
||
//! - Understand basic Rust Arc (shared ownership)
|
||
//! - Understand Clone trait
|
||
//!
|
||
//! KEY CONCEPTS:
|
||
//! - T: Clone requirement means each listener clones the value
|
||
//! - For large types, cloning is expensive
|
||
//! - Arc<T>: Clone just increments refcount (cheap!)
|
||
//! - Memory: One allocation, many references
|
||
//!
|
||
//! Run with: cargo run --example 04_efficient_broadcasting
|
||
|
||
use announcement::Announcement;
|
||
use std::sync::Arc;
|
||
|
||
// Define some example types of different sizes
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
struct SmallData {
|
||
value: i32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
#[allow(dead_code)]
|
||
struct MediumData {
|
||
buffer: [u8; 1024], // 1 KB
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
struct LargeData {
|
||
buffer: Vec<u8>, // Could be many MB
|
||
metadata: String,
|
||
}
|
||
|
||
fn main() {
|
||
println!("=== Efficient Broadcasting with Arc ===\n");
|
||
|
||
// STEP 1: The Clone requirement
|
||
// ==============================
|
||
// Remember: Announcement requires T: Clone
|
||
// This means each listener gets a CLONED copy of the value.
|
||
//
|
||
// For small types (like i32), this is fine:
|
||
// - i32: 4 bytes, copying is trivial
|
||
//
|
||
// For large types, this can be expensive:
|
||
// - Vec<u8> with 10MB: Cloning allocates 10MB per listener!
|
||
println!("Step 1: Understanding the Clone requirement\n");
|
||
println!(" Announcement<T> requires T: Clone");
|
||
println!(" Each listener.try_listen() or .listen().await CLONES T");
|
||
println!(" For large T, this can be expensive!\n");
|
||
|
||
// STEP 2: Example - Small data WITHOUT Arc
|
||
// =========================================
|
||
// For small types, direct usage is fine and efficient.
|
||
println!("Step 2: Small data WITHOUT Arc (efficient):");
|
||
|
||
let (announcer, announcement) = Announcement::<SmallData>::new();
|
||
let small_data = SmallData { value: 42 };
|
||
|
||
println!(
|
||
" Size of SmallData: {} bytes",
|
||
std::mem::size_of::<SmallData>()
|
||
);
|
||
println!(" Creating 3 listeners...");
|
||
|
||
let l1 = announcement.listener();
|
||
let l2 = announcement.listener();
|
||
let l3 = announcement.listener();
|
||
|
||
announcer.announce(small_data.clone()).unwrap();
|
||
|
||
let r1 = l1.try_listen().unwrap();
|
||
let _r2 = l2.try_listen().unwrap();
|
||
let _r3 = l3.try_listen().unwrap();
|
||
|
||
println!(" All listeners received: value={}", r1.value);
|
||
println!(" Cost: 3 clones of 4 bytes each = 12 bytes total");
|
||
println!(" Verdict: ✓ Direct usage is fine for small types\n");
|
||
|
||
// STEP 3: Example - Large data WITHOUT Arc (INEFFICIENT!)
|
||
// ========================================================
|
||
// For large types, direct cloning wastes memory and time.
|
||
println!("Step 3: Large data WITHOUT Arc (INEFFICIENT):");
|
||
|
||
let (announcer, announcement) = Announcement::<LargeData>::new();
|
||
|
||
// Create a 1MB buffer
|
||
let large_buffer = vec![42u8; 1024 * 1024]; // 1 MB
|
||
let large_data = LargeData {
|
||
buffer: large_buffer,
|
||
metadata: "This is large data".to_string(),
|
||
};
|
||
|
||
let data_size = large_data.buffer.len();
|
||
println!(" Size of LargeData: ~{} bytes", data_size);
|
||
println!(" Creating 3 listeners...");
|
||
|
||
let l1 = announcement.listener();
|
||
let l2 = announcement.listener();
|
||
let l3 = announcement.listener();
|
||
|
||
announcer.announce(large_data).unwrap();
|
||
|
||
let r1 = l1.try_listen().unwrap();
|
||
let _r2 = l2.try_listen().unwrap();
|
||
let _r3 = l3.try_listen().unwrap();
|
||
|
||
println!(" All listeners received {} byte buffer", r1.buffer.len());
|
||
println!(
|
||
" Cost: 3 clones of {} bytes each = {} bytes total",
|
||
data_size,
|
||
data_size * 3
|
||
);
|
||
println!(" Verdict: ✗ WASTEFUL - we allocated 3MB when we only need 1MB!\n");
|
||
|
||
// STEP 4: The Solution - Arc<T>
|
||
// ==============================
|
||
// Wrap large types in Arc to avoid expensive clones.
|
||
//
|
||
// IMPORTANT: Arc<T>: Clone just increments a reference count!
|
||
// - Original data: 1 allocation (1MB in this example)
|
||
// - Each clone: Just increment counter (~5ns, no allocation)
|
||
// - Memory: Shared, not duplicated
|
||
println!("Step 4: Large data WITH Arc (EFFICIENT!):");
|
||
|
||
// NOTE: We wrap LargeData in Arc
|
||
let (announcer, announcement) = Announcement::<Arc<LargeData>>::new();
|
||
|
||
let large_buffer = vec![99u8; 1024 * 1024]; // 1 MB
|
||
let large_data = Arc::new(LargeData {
|
||
buffer: large_buffer,
|
||
metadata: "This is Arc'd large data".to_string(),
|
||
});
|
||
|
||
println!(" Size of wrapped data: ~{} bytes", large_data.buffer.len());
|
||
println!(
|
||
" Size of Arc pointer: {} bytes",
|
||
std::mem::size_of::<Arc<LargeData>>()
|
||
);
|
||
println!(" Creating 3 listeners...");
|
||
|
||
let l1 = announcement.listener();
|
||
let l2 = announcement.listener();
|
||
let l3 = announcement.listener();
|
||
|
||
// IMPORTANT: We clone the Arc before announcing, to keep a reference
|
||
announcer.announce(Arc::clone(&large_data)).unwrap();
|
||
|
||
let r1 = l1.try_listen().unwrap();
|
||
let r2 = l2.try_listen().unwrap();
|
||
let r3 = l3.try_listen().unwrap();
|
||
|
||
println!(" All listeners received {} byte buffer", r1.buffer.len());
|
||
println!(
|
||
" Cost: 3 clones of Arc pointer = {} bytes total",
|
||
std::mem::size_of::<Arc<LargeData>>() * 3
|
||
);
|
||
println!(" Verdict: ✓ EFFICIENT - only 1MB allocated, shared among all!\n");
|
||
|
||
// STEP 5: Verifying Arc refcount
|
||
// ===============================
|
||
// Arc keeps a reference count. Let's verify memory is shared.
|
||
println!("Step 5: Verifying Arc reference counting:");
|
||
|
||
println!(
|
||
" Original Arc refcount: {}",
|
||
Arc::strong_count(&large_data)
|
||
);
|
||
println!(" Expected: 5 (original + announcement + r1 + r2 + r3)");
|
||
|
||
// NOTE: Actually we have:
|
||
// - original (large_data variable)
|
||
// - stored in announcement's OnceLock
|
||
// - r1, r2, r3 (cloned by listeners)
|
||
// = 5 total references
|
||
|
||
let actual_count = Arc::strong_count(&r1);
|
||
println!(" Actual count via r1: {}", actual_count);
|
||
|
||
if actual_count == 5 {
|
||
println!(" ✓ Correct! All references point to the same allocation\n");
|
||
}
|
||
|
||
// Verify all Arc's point to the same data
|
||
println!(" Verifying all listeners have same data:");
|
||
println!(" r1.metadata: {}", r1.metadata);
|
||
println!(" r2.metadata: {}", r2.metadata);
|
||
println!(" r3.metadata: {}", r3.metadata);
|
||
println!(" Arc::ptr_eq(&r1, &r2): {}", Arc::ptr_eq(&r1, &r2));
|
||
println!(" ✓ Same memory address - truly shared!\n");
|
||
|
||
// STEP 6: Performance comparison
|
||
// ===============================
|
||
println!("Step 6: Performance comparison:");
|
||
println!("\n Direct Clone (1MB data, 100 listeners):");
|
||
println!(" - Memory: 100 MB (100 allocations)");
|
||
println!(" - Time: ~100ms (100 × 1ms per clone)");
|
||
println!(" - CPU cache: Poor (scattered allocations)");
|
||
|
||
println!("\n Arc Clone (1MB data, 100 listeners):");
|
||
println!(" - Memory: 1 MB (1 allocation)");
|
||
println!(" - Time: ~500ns (100 × 5ns per refcount)");
|
||
println!(" - CPU cache: Good (shared memory)");
|
||
|
||
println!("\n Speedup: ~200,000x faster!");
|
||
println!(" Memory saved: 99 MB\n");
|
||
|
||
// STEP 7: When to use Arc
|
||
// ========================
|
||
println!("Step 7: Decision guide - When to use Arc:");
|
||
println!("\n Use DIRECT value when:");
|
||
println!(" ✓ Type is small (< 16 bytes)");
|
||
println!(" ✓ Type is Copy (i32, f64, etc.)");
|
||
println!(" ✓ Only 1-2 listeners");
|
||
println!(" ✓ Clone is cheap");
|
||
|
||
println!("\n Use Arc<T> when:");
|
||
println!(" ✓ Type is large (> 16 bytes)");
|
||
println!(" ✓ Clone is expensive");
|
||
println!(" ✓ Many listeners (3+)");
|
||
println!(" ✓ Type contains Vec, String, HashMap, etc.");
|
||
|
||
println!("\n RULE OF THUMB:");
|
||
println!(" For > 1 listener AND size > 8 bytes: Use Arc<T>\n");
|
||
|
||
// STEP 8: Example with complex type
|
||
// ==================================
|
||
#[derive(Clone, Debug)]
|
||
#[allow(dead_code)]
|
||
struct Config {
|
||
database_url: String,
|
||
api_keys: Vec<String>,
|
||
settings: std::collections::HashMap<String, String>,
|
||
}
|
||
|
||
println!("Step 8: Real-world example - Configuration broadcast:");
|
||
|
||
let (announcer, announcement) = Announcement::<Arc<Config>>::new();
|
||
|
||
let config = Arc::new(Config {
|
||
database_url: "postgresql://localhost/mydb".to_string(),
|
||
api_keys: vec!["key1".to_string(), "key2".to_string(), "key3".to_string()],
|
||
settings: [
|
||
("timeout".to_string(), "30".to_string()),
|
||
("retry".to_string(), "3".to_string()),
|
||
]
|
||
.iter()
|
||
.cloned()
|
||
.collect(),
|
||
});
|
||
|
||
println!(" Created complex Config with String, Vec, HashMap");
|
||
println!(" Wrapped in Arc for efficient sharing");
|
||
|
||
// Create several "workers" that need the config
|
||
let worker_listeners: Vec<_> = (0..10).map(|_| announcement.listener()).collect();
|
||
|
||
announcer.announce(Arc::clone(&config)).unwrap();
|
||
|
||
println!(" Broadcasted config to 10 workers");
|
||
println!(" Memory: 1 allocation, shared by all");
|
||
println!(" Time: ~100ns total (10 × 10ns Arc clone)\n");
|
||
|
||
// Workers can all access the config
|
||
for (i, listener) in worker_listeners.iter().enumerate() {
|
||
let worker_config = listener.try_listen().unwrap();
|
||
println!(" Worker {}: DB = {}", i, worker_config.database_url);
|
||
}
|
||
|
||
println!();
|
||
|
||
// SUMMARY
|
||
// =======
|
||
println!("=== Summary ===");
|
||
println!("✓ Understood why T: Clone can be expensive");
|
||
println!("✓ Used Arc<T> to avoid expensive clones");
|
||
println!("✓ Verified Arc reference counting");
|
||
println!("✓ Compared performance: Direct vs Arc");
|
||
println!("✓ Applied to real-world configuration broadcast");
|
||
println!("\nKEY TAKEAWAYS:");
|
||
println!(" 1. Announcement<T> clones T for each listener");
|
||
println!(" 2. Arc<T>: Clone is cheap (just refcount increment)");
|
||
println!(" 3. Use Arc<T> for types > 8 bytes with multiple listeners");
|
||
println!(" 4. Memory efficiency: 1 allocation shared by all");
|
||
println!(" 5. Time efficiency: ~5ns Arc clone vs ms for large clones");
|
||
println!("\nNEXT: See example 05 for blocking operations");
|
||
|
||
// EXPECTED OUTPUT:
|
||
// ================
|
||
// You should see:
|
||
// - Direct small data usage (efficient)
|
||
// - Direct large data usage (wasteful - 3MB allocated)
|
||
// - Arc large data usage (efficient - 1MB allocated)
|
||
// - Arc refcount verification showing shared memory
|
||
// - Performance comparison showing massive speedup
|
||
// - Real-world config broadcast example
|
||
}
|