Select async/concurrency primitives based on your specific usage patterns and requirements rather than defaulting to generic solutions. Different async contexts benefit from different tools:

Cancellation patterns: Use dedicated utilities like CancellationToken::run_until_cancelled() instead of manual tokio::select! loops when you need simple cancellation logic.

// Instead of manual select loops:
loop {
    tokio::select! {
        Ok((stream, _)) = listener.accept() => { /* handle */ }
        _ = token.cancelled() => { break; }
    }
}

// Use the dedicated utility:
token.run_until_cancelled(async {
    // your async logic here
}).await;

Data flow patterns: Prefer Streams over channels when you need concurrent message processing and better debugging capabilities. Streams allow for easier composition with .inspect(), .for_each_concurrent(), and other combinators.

// Instead of channels:
let (tx, mut rx) = mpsc::channel(chunk_size);
fetch_range(tx, slice).await?;
while let Some(message) = rx.recv().await { /* handle */ }

// Use streams:
let stream = fetch_range(slice).instrument(info_span!("fetch"));
stream.for_each_concurrent(None, |message| handle_message(message)).await;

Synchronization: Use async-aware locks (like tokio::sync::RwLock) only when you need to hold the lock across await points. For short, synchronous critical sections, prefer std::sync or parking_lot locks for better performance.

// Use async locks only when holding across awaits:
let guard = async_lock.lock().await;
some_async_operation().await; // Lock held across await
drop(guard);

// Use sync locks for quick operations:
{
    let guard = sync_lock.lock().unwrap();
    quick_synchronous_work();
} // Lock automatically released

This approach reduces complexity, improves performance, and makes code more maintainable by matching the tool to the specific concurrency requirements.