| name | rust-async-concurrency |
| description | Manage concurrent operations with channels, semaphores, locks, and streams. Use when coordinating parallel work or limiting resource usage. |
Async Concurrency
Patterns for managing concurrent operations in async Rust.
Channel Selection
Choose the right channel for your use case:
use tokio::sync::{mpsc, oneshot, broadcast, watch};
let (tx, mut rx) = mpsc::channel::<Task>(100);
let (tx, rx) = oneshot::channel::<Result>();
let (tx, _rx) = broadcast::channel::<Event>(16);
let (tx, rx) = watch::channel(initial_config);
Semaphore for Resource Limiting
use std::sync::Arc;
use tokio::sync::Semaphore;
let semaphore = Arc::new(Semaphore::new(10));
async fn limited_operation(semaphore: Arc<Semaphore>) -> Result<()> {
let _permit = semaphore.acquire().await?;
do_work().await?;
Ok(())
}
let vram_semaphore = Arc::new(Semaphore::new(16));
async fn run_model(semaphore: Arc<Semaphore>, vram_gb: u32) -> Result<()> {
let _permit = semaphore.acquire_many(vram_gb).await?;
run_gpu_model().await
}
Parallel Execution with join!
use tokio::join;
let (result_a, result_b, result_c) = join!(
fetch_a(),
fetch_b(),
fetch_c(),
);
let (a, b) = tokio::try_join!(
fetch_a(),
fetch_b(),
)?;
Parallel Streams
use futures::stream::{self, StreamExt};
let results: Vec<_> = stream::iter(items)
.map(|item| async move { process(item).await })
.buffer_unordered(10)
.collect()
.await;
let semaphore = Arc::new(Semaphore::new(10));
let results: Vec<_> = stream::iter(items)
.map(|item| {
let sem = semaphore.clone();
async move {
let _permit = sem.acquire().await?;
process(item).await
}
})
.buffer_unordered(100)
.collect()
.await;
Shared State with Locks
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
let shared = Arc::new(Mutex::new(State::new()));
async fn update(shared: Arc<Mutex<State>>) {
let mut guard = shared.lock().await;
guard.update();
}
let cache = Arc::new(RwLock::new(HashMap::new()));
async fn read(cache: Arc<RwLock<Cache>>) -> Option<Value> {
cache.read().await.get(&key).cloned()
}
async fn write(cache: Arc<RwLock<Cache>>, key: Key, value: Value) {
cache.write().await.insert(key, value);
}
async fn process(mutex: &Mutex<Data>) {
let data = {
mutex.lock()..()
};
(&data).;
}
Parking Lot for Sync Locks
use parking_lot::{Mutex, RwLock};
let state = Arc::new(Mutex::new(State::new()));
fn quick_update(state: &Mutex<State>) {
state.lock().counter += 1;
}
Batching with Select
use tokio::select;
use tokio::time::{interval, Duration};
async fn batch_processor(mut rx: mpsc::Receiver<Item>) {
let mut batch = Vec::with_capacity(100);
let mut flush_interval = interval(Duration::from_millis(100));
loop {
select! {
Some(item) = rx.recv() => {
batch.push(item);
if batch.len() >= 100 {
process_batch(&batch).await;
batch.clear();
}
}
_ = flush_interval.tick() => {
if !batch.is_empty() {
process_batch(&batch).await;
batch.clear();
}
}
}
}
}
Guidelines
- Use bounded channels for backpressure
- Prefer
buffer_unordered over sequential awaits
- Minimize lock scope in async code
- Consider channels over shared state
- Use semaphores for resource limiting
- Use
parking_lot for sync-only locks
Examples
See hercules-local-algo/src/pipeline/prefetch.rs for production patterns.