# Async/Await em Rust: Tokio, Futures e concorrência assíncrona explicados em profundidade > Mergulho profundo em async/await no Rust: o runtime Tokio, a trait Future, o lançamento de tarefas, a concorrência estruturada e os padrões práticos para construir aplicações assíncronas de alto desempenho. - Published: 2026-04-30 - Updated: 2026-04-30 - Author: SharpSkill - Tags: rust, async, tokio, futures, concurrency - Reading time: 12 min --- O sistema async/await do Rust oferece programação assíncrona com custo zero, mas diferentemente de linguagens como JavaScript ou Python, o Rust não inclui um runtime embutido. A trait `Future`, as palavras-chave `async`/`await` e um executor externo como o Tokio formam um sistema de três camadas que confere controle total sobre como o trabalho concorrente é agendado e executado. > **Conceito-chave** > > Uma `async fn` em Rust retorna uma máquina de estados que implementa `Future`. Nada é executado até que um executor (como o Tokio) faça o polling dessa future. Esse modelo de avaliação preguiçosa elimina alocações ocultas e fornece ao compilador informações suficientes para otimizar de forma agressiva. ## Como as Futures do Rust diferem de outras linguagens Em JavaScript, chamar uma função `async` inicia a execução imediatamente e retorna uma `Promise`. Em Rust, chamar uma `async fn` não faz nada — constrói um valor `Future` que permanece inativo até ser sondado. Essa distinção tem consequências práticas significativas. A trait `Future` define um único método: ```rust // core::future::Future trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; } ``` `Poll::Ready(value)` sinaliza a conclusão. `Poll::Pending` indica ao executor para estacionar a tarefa e acordá-la mais tarde através do `Waker` armazenado em `Context`. O executor nunca faz espera ativa — ele se apoia em mecanismos do sistema operacional (epoll no Linux, kqueue no macOS) para saber quando uma operação de I/O está pronta. Esse modelo baseado em polling significa que as futures do Rust são máquinas de estados compiladas em tempo de construção, não cadeias de callbacks alocadas no heap. O compilador transforma cada ponto `.await` em uma variante de estado, produzindo código que rivaliza com máquinas de estados escritas manualmente em termos de desempenho. ## Configurando o Tokio como runtime assíncrono O Tokio é o runtime assíncrono mais amplamente adotado no ecossistema Rust. Em 2026, o Tokio 2.0 introduziu um agendador aprimorado com work-stealing e uma roda de temporização hierárquica que reduzem a sobrecarga em aproximadamente 40% em relação a versões anteriores. Uma configuração mínima requer dois elementos no `Cargo.toml`: ```toml # Cargo.toml [dependencies] tokio = { version = "2", features = ["rt-multi-thread", "macros", "net", "time"] } ``` A macro `#[tokio::main]` transforma a função `main` em um ponto de entrada assíncrono: ```rust // main.rs #[tokio::main] async fn main() { let result = fetch_data().await; println!("Got: {result}"); } async fn fetch_data() -> String { // Simulate async I/O tokio::time::sleep(std::time::Duration::from_millis(100)).await; String::from("data loaded") } ``` Nos bastidores, `#[tokio::main]` se expande para uma chamada `Runtime::new()` com um agendador multi-thread. Para aplicações que precisam de apenas uma thread (ferramentas CLI, scripts), `#[tokio::main(flavor = "current_thread")]` evita a sobrecarga da sincronização entre threads. ## Lançamento de tarefas e concorrência estruturada Chamadas `.await` sequenciais executam uma após a outra. Para executar trabalho de forma concorrente, o Tokio fornece `tokio::spawn`, que agenda uma future no pool de threads do runtime: ```rust // concurrent_tasks.rs use tokio::task::JoinHandle; #[tokio::main] async fn main() { // Spawn two independent tasks let handle_a: JoinHandle = tokio::spawn(async { expensive_computation("dataset_a").await }); let handle_b: JoinHandle = tokio::spawn(async { expensive_computation("dataset_b").await }); // Await both results let (result_a, result_b) = ( handle_a.await.expect("task A panicked"), handle_b.await.expect("task B panicked"), ); println!("Results: {result_a}, {result_b}"); } async fn expensive_computation(name: &str) -> u32 { tokio::time::sleep(std::time::Duration::from_secs(1)).await; println!("{name} done"); 42 } ``` Ambas as tarefas executam em paralelo através das threads disponíveis. O `JoinHandle` retornado por `spawn` atua como uma future que se resolve quando a tarefa lançada termina. Uma restrição fundamental: `tokio::spawn` exige que a future seja `'static` — ela não pode emprestar variáveis locais. Isso força uma transferência de propriedade explícita, o que previne corridas de dados em tempo de compilação. > **Requisito Send + 'static** > > Tarefas lançadas devem ser `Send + 'static`. Dados compartilhados entre pontos `.await` dentro de uma tarefa lançada precisam ser `Clone`ados para a tarefa ou envolvidos em `Arc`. Tentar emprestar referências locais da pilha além da fronteira de um spawn gera um erro de compilação. ## Unindo múltiplas Futures com tokio::join! e tokio::select! Para um número fixo de operações concorrentes, `tokio::join!` executa todas as futures até a conclusão e retorna uma tupla de resultados: ```rust // join_example.rs use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { // All three run concurrently, total time ~200ms (not 600ms) let (users, orders, inventory) = tokio::join!( fetch_users(), fetch_orders(), fetch_inventory() ); println!("Users: {}, Orders: {}, Stock: {}", users.len(), orders.len(), inventory); } async fn fetch_users() -> Vec { sleep(Duration::from_millis(200)).await; vec!["Alice".into(), "Bob".into()] } async fn fetch_orders() -> Vec { sleep(Duration::from_millis(150)).await; vec!["ORD-001".into()] } async fn fetch_inventory() -> u32 { sleep(Duration::from_millis(100)).await; 84 } ``` Diferentemente de `spawn`, `join!` não exige `'static` — as futures emprestam livremente do escopo que as contém. Isso o torna a escolha preferida quando todos os ramos precisam ser concluídos. `tokio::select!` aguarda a *primeira* future a ser concluída e cancela as demais: ```rust // select_example.rs use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { tokio::select! { val = fetch_from_cache() => { println!("Cache hit: {val}"); } val = fetch_from_database() => { println!("DB result: {val}"); } } } async fn fetch_from_cache() -> String { sleep(Duration::from_millis(5)).await; "cached_value".into() } async fn fetch_from_database() -> String { sleep(Duration::from_millis(50)).await; "db_value".into() } ``` O ramo perdedor é descartado, o que executa os destruidores e libera os recursos. Esse padrão é essencial para implementar tempos limite, cancelamento e competição entre fontes de dados. ## Tratamento de erros em Rust assíncrono Funções assíncronas se compõem naturalmente com o tipo `Result` do Rust. O operador `?` funciona dentro de uma `async fn` exatamente como em código síncrono: ```rust // error_handling.rs use std::io; #[derive(Debug)] enum AppError { Network(reqwest::Error), Parse(serde_json::Error), Io(io::Error), } async fn load_config(url: &str) -> Result { let response = reqwest::get(url) .await .map_err(AppError::Network)?; let text = response.text() .await .map_err(AppError::Network)?; let config: Config = serde_json::from_str(&text) .map_err(AppError::Parse)?; Ok(config) } #[derive(serde::Deserialize)] struct Config { db_url: String, port: u16, } ``` Cada ponto `.await` é um ponto de suspensão potencial e um local de erro potencial. Mapear os erros explicitamente mantém o fluxo de controle assíncrono legível e evita o "callback hell" encontrado em outros modelos assíncronos. ## Canais para comunicação assíncrona entre tarefas O Tokio fornece canais compatíveis com a assincronia que permitem às tarefas lançadas se comunicarem sem estado mutável compartilhado: ```rust // channel_example.rs use tokio::sync::mpsc; #[tokio::main] async fn main() { // Bounded channel with capacity 32 let (tx, mut rx) = mpsc::channel::(32); // Producer task let producer = tokio::spawn(async move { for i in 0..5 { tx.send(format!("message-{i}")).await.unwrap(); tokio::time::sleep(std::time::Duration::from_millis(10)).await; } // tx dropped here, closing the channel }); // Consumer reads until channel closes while let Some(msg) = rx.recv().await { println!("Received: {msg}"); } producer.await.unwrap(); } ``` `mpsc::channel` cria um canal multi-produtor, consumidor único. Para cenários de produtor único, `oneshot::channel` oferece uma opção mais leve. Para transmitir para múltiplos consumidores, `broadcast::channel` clona as mensagens para cada receptor ativo. A capacidade limitada (32 neste exemplo) aplica contrapressão: quando o buffer enche, `send().await` suspende o produtor até que o consumidor se atualize. Isso previne o crescimento descontrolado de memória em pipelines de alta vazão. ## Padrão do mundo real: requisições HTTP concorrentes com limitação de taxa Um cenário comum em produção envolve fazer muitas requisições HTTP de forma concorrente respeitando os limites de taxa. `tokio::sync::Semaphore` controla o grau de paralelismo: ```rust // rate_limited_fetcher.rs use std::sync::Arc; use tokio::sync::Semaphore; async fn fetch_all(urls: Vec, max_concurrent: usize) -> Vec> { let semaphore = Arc::new(Semaphore::new(max_concurrent)); let mut handles = Vec::new(); for url in urls { let sem = Arc::clone(&semaphore); let handle = tokio::spawn(async move { // Acquire permit before making request let _permit = sem.acquire().await.unwrap(); reqwest::get(&url) .await .map(|r| r.status().to_string()) .map_err(|e| e.to_string()) // permit dropped here, allowing next task to proceed }); handles.push(handle); } let mut results = Vec::new(); for handle in handles { results.push(handle.await.unwrap()); } results } ``` O semáforo limita as requisições ativas a `max_concurrent`. Todas as tarefas são lançadas imediatamente, mas apenas N executam sua chamada HTTP em qualquer momento. Quando uma tarefa termina e libera sua permissão, outra tarefa acorda e prossegue. > **Escolhendo a primitiva de concorrência certa** > > `tokio::join!` se adapta a um conjunto fixo e conhecido de futures. `tokio::spawn` + `Semaphore` se adapta a uma coleção dinâmica e potencialmente grande. `select!` se adapta a cenários de corrida ou tempo limite. Escolher a primitiva certa evita tanto a subutilização quanto o esgotamento de recursos. ## Pin e Unpin: por que o Rust assíncrono precisa deles O tipo `Pin` aparece na assinatura de `Future::poll` por uma razão específica. Máquinas de estados assíncronas podem conter campos autorreferenciais — uma referência em um estado apontando para dados armazenados em outra parte da mesma estrutura. Mover a estrutura na memória invalidaria essa referência. `Pin<&mut Self>` garante que a future não será movida após o início do polling. A maioria das futures escritas pelos desenvolvedores são `Unpin` (movíveis), e o compilador lida com o pinning automaticamente. O gerenciamento manual de `Pin` só se torna necessário ao implementar tipos `Future` personalizados ou ao trabalhar com padrões `async_stream`. Para o uso diário do Rust assíncrono, o ponto essencial: `Box::pin(future)` converte qualquer future em uma forma fixada e alocada no heap que pode ser armazenada em coleções ou retornada de métodos de traits. ## Características de desempenho e quando usar async O Rust assíncrono brilha em cargas de trabalho limitadas por I/O: servidores HTTP, clientes de banco de dados, brokers de mensagens, observadores de arquivos. Cada tarefa usa apenas algumas centenas de bytes de pilha (comparado com os 8 MB padrão das threads do sistema operacional), tornando viável executar centenas de milhares de tarefas concorrentes em uma única máquina. Para trabalho limitado por CPU, `tokio::task::spawn_blocking` descarrega o cálculo para um pool de threads dedicado, evitando que o executor assíncrono fique sem recursos: ```rust // spawn_blocking_example.rs #[tokio::main] async fn main() { let hash = tokio::task::spawn_blocking(|| { // CPU-intensive work runs on a blocking thread compute_hash(b"large dataset") }) .await .unwrap(); println!("Hash: {hash}"); } fn compute_hash(data: &[u8]) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); data.hash(&mut hasher); format!("{:x}", hasher.finish()) } ``` Combinar `spawn_blocking` com I/O assíncrono mantém o loop de eventos responsivo enquanto aproveita todos os núcleos da CPU para computações pesadas. ## Conclusão - As futures do Rust são máquinas de estados preguiçosas — nada é executado até que um executor faça o polling, conferindo controle total sobre o agendamento e o uso de recursos - O Tokio 2.0 fornece o runtime, com `spawn` para tarefas independentes, `join!` para conclusão paralela e `select!` para competição - A restrição `Send + 'static` nas tarefas lançadas detecta corridas de dados em tempo de compilação, trocando um pouco de fricção ergonômica por segurança de threads garantida - Os canais (`mpsc`, `oneshot`, `broadcast`) desacoplam a comunicação entre tarefas sem estado mutável compartilhado - `Semaphore` e canais limitados fornecem contrapressão, prevenindo o esgotamento de recursos em sistemas de produção - `spawn_blocking` preenche a lacuna entre I/O assíncrono e computação limitada por CPU - Dominar essas primitivas — [a trait Future](https://doc.rust-lang.org/std/future/trait.Future.html), [o runtime Tokio](https://tokio.rs/) e as regras de propriedade — desbloqueia o potencial do Rust para construir serviços de alta vazão e baixa latência --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/rust/rust-async-await-tokio-futures-concurrency