Rust by Example: Rate Limited Tasks
Control the flow of your async tasks with Rate Limiting. Learn to use Semaphores to restrict the number of concurrent operations, preventing resource exhaustion in high-load scenarios.
Code
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// Allow max 3 concurrent tasks
let semaphore = Arc::new(Semaphore::new(3));
let mut handles = vec![];
for i in 0..10 {
let permit = semaphore.clone();
handles.push(tokio::spawn(async move {
// Acquire a permit. If 3 tasks are already running,
// this will wait until one finishes.
let _permit = permit.acquire().await.unwrap();
println!("Task {} started", i);
sleep(Duration::from_millis(500)).await;
println!("Task {} finished", i);
// _permit is dropped here, releasing the slot
}));
}
// Wait for all tasks
for handle in handles {
handle.await.unwrap();
}
}Explanation
When spawning thousands of tasks, you often need to limit how many run at once to avoid overwhelming a server or exhausting system resources (like file descriptors or database connections). A Semaphore is the standard tool for this.
A Semaphore maintains a set of "permits". Before performing a restricted action, a task must acquire a permit. If no permits are available, the task waits (asynchronously). When the task is done, it drops the permit, returning it to the semaphore for another task to use.
In Tokio, Semaphore is async-aware. Calling acquire().await suspends the task without blocking the thread, allowing other tasks to make progress while waiting for a slot.
Code Breakdown
Semaphore::new(3). Initializes the semaphore with 3 permits. This means at most 3 tasks can hold a permit simultaneously. We wrap it in Arc to share it.permit.acquire().await. This line enforces the limit. It returns a SemaphorePermit which is an RAII guard. If 3 permits are taken, this awaits.let _permit. We bind the permit to a variable to keep it alive. If we didn't bind it (e.g., let _ = ...), it would be dropped immediately, releasing the lock too early._permit is dropped. When the async block ends, the _permit variable goes out of scope. Its Drop implementation automatically adds 1 back to the semaphore counter.
