Files
announcement/examples/04_efficient_broadcasting.rs
T
swaits e8e55b7676 chore: initial release v0.1.0
Runtime-agnostic oneshot broadcast channel for Rust.
2025-10-13 19:36:37 -06:00

309 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! ============================================================================
//! 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
}