//! ============================================================================ //! LEARNING GUIDE - PART 4: Efficient Broadcasting with Arc //! ============================================================================ //! //! WHAT YOU'LL LEARN: //! - Why Arc is important for efficient broadcasting //! - How to avoid expensive clones with large data //! - Memory efficiency with Arc reference counting //! - When to use Arc 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: 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, // 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 with 10MB: Cloning allocates 10MB per listener! println!("Step 1: Understanding the Clone requirement\n"); println!(" Announcement 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::::new(); let small_data = SmallData { value: 42 }; println!( " Size of SmallData: {} bytes", std::mem::size_of::() ); 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::::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 // ============================== // Wrap large types in Arc to avoid expensive clones. // // IMPORTANT: Arc: 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::>::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::>() ); 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::>() * 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 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\n"); // STEP 8: Example with complex type // ================================== #[derive(Clone, Debug)] #[allow(dead_code)] struct Config { database_url: String, api_keys: Vec, settings: std::collections::HashMap, } println!("Step 8: Real-world example - Configuration broadcast:"); let (announcer, announcement) = Announcement::>::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 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 clones T for each listener"); println!(" 2. Arc: Clone is cheap (just refcount increment)"); println!(" 3. Use Arc 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 }