use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
#[derive(Default)]
struct Metrics {
per_endpoint: HashMap<String, u64>,
total: u64,
}
#[derive(Clone)]
pub struct SharedMetrics {
inner: Arc<Mutex<Metrics>>,
}
impl SharedMetrics {
pub fn new() -> Self {
SharedMetrics {
inner: Arc::new(Mutex::new(Metrics::default())),
}
}
pub fn record(&self, endpoint: &str) {
let mut guard = self
.inner
.lock()
.unwrap_or_else(PoisonError::into_inner);
*guard.per_endpoint.entry(endpoint.to_string()).or_insert(0) += 1;
guard.total += 1;
}
pub fn snapshot(&self) -> (Vec<(String, u64)>, u64) {
let guard = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
let mut rows: Vec<(String, u64)> =
guard.per_endpoint.iter().map(|(k, v)| (k.clone(), *v)).collect();
rows.sort_by(|a, b| b.1.cmp(&a.1));
(rows, guard.total)
}
}
use std::thread::{self, JoinHandle};
use std::time::Duration;
use crate::metrics::SharedMetrics;
const ENDPOINTS: [&str; 3] = ["/login", "/search", "/checkout"];
pub fn spawn_workers(metrics: &SharedMetrics, count: usize, per_worker: usize) -> Vec<JoinHandle<()>> {
(0..count)
.map(|worker_id| {
let metrics = metrics.clone();
thread::spawn(move || {
for i in 0..per_worker {
let endpoint = ENDPOINTS[(worker_id + i) % ENDPOINTS.len()];
metrics.record(endpoint);
thread::sleep(Duration::from_micros(50));
}
})
})
.collect()
}
mod metrics;
mod worker;
use metrics::SharedMetrics;
fn main() {
let metrics = SharedMetrics::new();
let handles = worker::spawn_workers(&metrics, 8, 500);
for handle in handles {
if handle.join().is_err() {
eprintln!("a worker thread panicked; metrics may be partial");
}
}
let (rows, total) = metrics.snapshot();
println!("total requests: {}", total);
for (endpoint, count) in rows {
println!(" {:<12} {}", endpoint, count);
}
}
This snippet builds a small in-memory metrics collector that several worker threads update concurrently. The central concept is Rust's shared-ownership-plus-locking idiom: Arc<Mutex<T>>. Arc (atomically reference-counted) lets multiple threads own the same allocation, and Mutex guarantees that only one thread mutates the inner data at a time. Neither alone is enough — Arc<T> gives shared reads but not mutation, and a bare Mutex cannot be sent to threads that outlive the current stack frame, so they are composed.
In metrics.rs, Metrics wraps a HashMap counting requests per endpoint plus a running total. The public API is a cheap-to-clone SharedMetrics handle: cloning it only bumps the Arc refcount, so every thread shares one underlying map. The record method locks, mutates, and releases in a tight scope. Notably lock() returns a Result because a Mutex becomes poisoned if a thread panics while holding it; the code calls unwrap_or_else with PoisonError::into_inner to recover the data rather than propagate the panic, which is the pragmatic choice for a metrics counter where a slightly-inconsistent read is acceptable.
The snapshot method demonstrates minimizing lock hold time: it clones the inner state under the lock and returns immediately, so formatting and I/O happen outside the critical section. Holding a lock across expensive work is the classic contention pitfall this avoids.
In worker.rs, spawn_workers clones the SharedMetrics handle once per thread before thread::spawn, because each closure must own its own handle with a 'static lifetime. The JoinHandles are collected and joined so the program waits for all recorded work.
main.rs wires it together: it constructs one SharedMetrics, hands clones to the workers, joins them, and prints the aggregated snapshot. The trade-off of a Mutex versus per-field atomics is deliberate — a Mutex keeps the multi-field update (map entry plus total) atomic as a unit, which atomics cannot easily do without racing between two separate operations. When contention becomes a bottleneck, sharding the map or switching to atomics per counter is the natural next step.
Related snips
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
Share this code
Here's the card — post it anywhere.