Desenvolvedores Python que estão migrando para Rust frequentemente encontram desafios interessantes quando o assunto é concorrência e paralelismo. Enquanto Python tem seu próprio conjunto de ferramentas (threads, multiprocessing, asyncio), Rust oferece uma abordagem única que combina segurança de memória com alto desempenho.
Neste artigo, vamos explorar como Rust aborda essas questões fundamentais da programação moderna, sempre comparando com os conceitos que você já conhece do Python.
A programação concorrente é como coordenar vários cozinheiros em uma mesma cozinha:
A grande diferença é que Rust garante em tempo de compilação que não haverá conflitos, enquanto Python precisa confiar no desenvolvedor para evitar race conditions.
# Python: Race condition comum
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
import threading
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # Resultado imprevisível - não será 1.000.000!
Rust previne esse problema em tempo de compilação:
// Rust: Tentativa ingênua que não compila
let mut counter = 0;
let handle = std::thread::spawn(|| {
// ERRO: `closure may outlive the current function, but it borrows
// `counter`, which is owned by the current function`
for _ in 0..100000 {
counter += 1;
}
});
handle.join().unwrap();
println!("{}", counter);
O compilador Rust nos impede de cometer erros comuns de concorrência! 🛡️
Enquanto em Python threads podem compartilhar estado livremente (com riscos), Rust exige que você pense cuidadosamente sobre o compartilhamento de dados.
# Python: Compartilhamento "fácil" mas perigoso
import threading
shared_data = {"counter": 0}
def worker():
for _ in range(1000):
shared_data["counter"] += 1
threads = []
for _ in range(10):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print(shared_data["counter"]) # Resultado inconsistente
Para compartilhar dados entre threads com segurança em Rust, precisamos de dois conceitos:
Mutex<T> (MUTual EXclusion): funciona como o threading.Lock() do Python. O Mutex garante que apenas um owner por vez possa acessar e modificar o dado interno.Arc<T> (Atomic Reference Counter): é como um Rc (contador de referências) seguro para threads — ele permite que múltiplos owners compartilhem a posse do mesmo valor. Não tem equivalente direto simples em Python, pois o GIL faz isso implicitamente.Juntos, Arc<Mutex<T>> é o padrão clássico para compartilhar estado mutável entre threads de forma segura: o Arc compartilha a propriedade e o Mutex protege o acesso ao dado interno.
// Rust: Compartilhamento seguro com Arc<Mutex<T>>
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
// O `move` força o closure a *tomar posse* (ownership) da variável `counter`
// (que é uma `Arc<Mutex<...>>`, já clonada para esta iteração).
// Isso é necessário porque as threads podem durar mais que a função atual —
// o sistema de ownership garante que os dados clonados vivam enquanto a thread precisar deles.
let handle = thread::spawn(move || {
for _ in 0..1000 {
let mut num = counter.lock().unwrap();
*num += 1;
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *counter.lock().unwrap()); // Sempre 10000
}
O segredo da segurança das threads em Rust está no sistema de tipos:
Send: Permite que a propriedade de dados seja movida entre threads.Sync: Permite que dados sejam acessados por múltiplas threads simultaneamente através de referências imutáveis (&T). Um tipo T é Sync se &T é Send.Isso contrasta diretamente com o GIL do Python: enquanto o GIL protege tudo de forma global (limitando o paralelismo), os traits Send/Sync permitem proteção granular — você usa Mutex apenas nos dados que realmente precisam de exclusão mútua, o que é mais eficiente e explícito.
Esses traits são automaticamente implementados quando é seguro fazê-lo, e o compilador valida isso para você.
A assincronia em Rust é diferente do Python: não há GIL e as tarefas podem executar verdadeiramente em paralelo quando há múltiplos núcleos disponíveis.
# Python: asyncio com GIL
import asyncio
async def tarefa_lenta(nome, segundos):
print(f"{nome} iniciou")
await asyncio.sleep(segundos)
print(f"{nome} terminou")
async def main():
await asyncio.gather(
tarefa_lenta("Tarefa 1", 2),
tarefa_lenta("Tarefa 2", 1),
tarefa_lenta("Tarefa 3", 3)
)
asyncio.run(main())
Em Rust, async/await é mais sobre concorrência do que paralelismo — isso significa que uma única thread pode gerenciar milhares de operações de I/O concorrentes (como esperar respostas de rede) de forma eficiente, alternando entre elas. O paralelismo real (múltiplos núcleos trabalhando ao mesmo tempo) ainda é alcançado pelo runtime async (como o Tokio) ao distribuir essas tarefas por uma pool de threads de trabalho.
// Rust: async/await com tokio
use tokio::time::{sleep, Duration};
async fn tarefa_lenta(nome: &str, segundos: u64) {
println!("{} iniciou", nome);
sleep(Duration::from_secs(segundos)).await;
println!("{} terminou", nome);
}
#[tokio::main]
async fn main() {
let tarefa1 = tarefa_lenta("Tarefa 1", 2);
let tarefa2 = tarefa_lenta("Tarefa 2", 1);
let tarefa3 = tarefa_lenta("Tarefa 3", 3);
// `tokio::join!` aguarda todas as tarefas completarem (sem dependências extras)
tokio::join!(tarefa1, tarefa2, tarefa3);
}
Uma diferença fundamental: Python tem um runtime built-in para async (asyncio), enquanto Rust usa bibliotecas externas (tokio, async-std):
asyncio é parte da biblioteca padrão — solução oficial e únicaEsta abordagem dá mais flexibilidade ao programador Rust. Você pode escolher tokio para alta performance e um ecossistema rico de bibliotecas async, ou async-std para uma API mais próxima da stdlib. Em contraste, Python oferece o asyncio como solução única na biblioteca padrão.
O GIL do Python é provavelmente o maior contraste com Rust:
# Python: CPU-bound com threads (não escala)
import threading
import time
def trabalho_pesado():
n = 0
for i in range(10000000):
n += i
return n
# Threads para trabalho CPU-bound não escalam por causa do GIL
inicio = time.time()
threads = []
for _ in range(4):
t = threading.Thread(target=trabalho_pesado)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Threads Python: {time.time() - inicio:.2f}s")
Para trabalho CPU-bound em Python, você precisaria usar multiprocessing:
# Python: CPU-bound com multiprocessing
from multiprocessing import Pool
import time
def trabalho_pesado(_):
n = 0
for i in range(10000000):
n += i
return n
inicio = time.time()
with Pool(4) as p:
p.map(trabalho_pesado, range(4))
print(f"Multiprocessing Python: {time.time() - inicio:.2f}s")
Em Rust, threads escalam perfeitamente para trabalho CPU-bound:
// Rust: CPU-bound com threads (escala linearmente)
use std::thread;
use std::time::Instant;
fn trabalho_pesado() -> i64 {
let mut n = 0;
for i in 0..10000000 {
n += i as i64;
}
n
}
fn main() {
let inicio = Instant::now();
let mut handles = vec![];
for _ in 0..4 {
handles.push(thread::spawn(|| {
trabalho_pesado();
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Threads Rust: {:.2?}", inicio.elapsed());
}
Para operações de I/O (arquivos, rede, banco de dados), ambas as linguagens têm boas soluções:
# Python: I/O-bound com asyncio (eficiente)
import aiohttp
import asyncio
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["http://httpbin.org/get"] * 10
tasks = [fetch(url) for url in urls]
# asyncio.gather aceita uma lista dinâmica de tarefas com sintaxe concisa
responses = await asyncio.gather(*tasks)
return len(responses)
// Rust: I/O-bound com async (eficiente)
// Cargo.toml: reqwest = { version = "0.12", features = ["json"] }
use reqwest;
use tokio::task::JoinSet;
async fn fetch(url: &str) -> Result<String, reqwest::Error> {
reqwest::get(url).await?.text().await
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let urls = vec!["http://httpbin.org/get"; 10];
// JoinSet gerencia um conjunto dinâmico de tarefas — sem dependências extras
let mut set = JoinSet::new();
for url in urls {
set.spawn(async move { fetch(url).await });
}
let mut count = 0;
while let Some(result) = set.join_next().await {
result??;
count += 1;
}
// Nota: Python's asyncio.gather(*tasks) é mais conciso para listas dinâmicas.
// Em Rust, JoinSet (tokio) ou futures::future::join_all (crate `futures`) são as alternativas,
// contrabalançadas pela performance e pelo controle fino que Rust oferece.
println!("Total de respostas: {}", count);
Ok(())
}
Vamos implementar um servidor HTTP simples que pode lidar com múltiplas requisições concorrentemente.
# Python: Servidor simples com Flask
from flask import Flask
import time
import threading
app = Flask(__name__)
@app.route('/lenta/<int:segundos>')
def rota_lenta(segundos):
time.sleep(segundos) # Simula trabalho bloqueante
return f"Dormi {segundos}s no thread {threading.get_ident()}"
if __name__ == '__main__':
# Flask usa threads por padrão para lidar com concorrência
app.run(threaded=True, port=8000)
Este servidor pode lidar com múltiplas requisições usando threads, mas cada thread consome recursos significativos do sistema.
// Rust: Servidor async com Actix Web
use actix_web::{get, web, App, HttpServer, HttpResponse};
use std::time::Duration;
use tokio::time::sleep;
#[get("/lenta/{segundos}")]
async fn rota_lenta(segundos: web::Path<u64>) -> HttpResponse {
sleep(Duration::from_secs(*segundos)).await; // Não bloqueia o thread
HttpResponse::Ok().body(format!("Dormi {}s", segundos))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new().service(rota_lenta)
})
.bind("127.0.0.1:8080")?
.workers(4) // Número de threads de trabalho
.run()
.await
}
A versão Rust é muito mais eficiente em termos de recursos porque:
# Python: Teste de carga para os servidores
import requests
import time
from concurrent.futures import ThreadPoolExecutor # necessário para ThreadPoolExecutor
def testar_servidor(porta, url, num_requisicoes):
inicio = time.time()
# O parâmetro `_` recebe cada número do range mas é ignorado —
# o importante é disparar `num_requisicoes` chamadas concorrentes.
def fazer_requisicao(_):
return requests.get(f"http://localhost:{porta}{url}").status_code
with ThreadPoolExecutor(max_workers=100) as executor:
# Executa a função 'fazer_requisicao' para cada elemento no range
resultados = list(executor.map(fazer_requisicao, range(num_requisicoes)))
tempo_total = time.time() - inicio
print(f"Porta {porta}: {num_requisicoes} reqs em {tempo_total:.2f}s")
# Testar ambos
testar_servidor(8000, "/lenta/1", 100) # Flask com threads
testar_servidor(8080, "/lenta/1", 100) # Actix com async
rayonPara trabalho CPU-bound que envolve processar coleções e iteradores, a crate rayon é uma ferramenta fantástica e muito popular. Ela oferece paralelismo quase automático — basta trocar .iter() por .par_iter():
// Cargo.toml: rayon = "1"
use rayon::prelude::*;
fn main() {
let data = vec!;[1][2]
// Processa cada elemento em paralelo entre todos os núcleos disponíveis!
let resultados: Vec<_> = data.par_iter().map(|x| x * x).collect();
println!("{:?}", resultados); //[1]
}
Para Pythonistas, isso é comparável a usar multiprocessing.Pool com map, mas com uma API muito mais ergonômica e sem o overhead de processos separados.
| Cenário | Python (Threads) | Python (Async) | Rust (Threads) | Rust (Async) |
|---|---|---|---|---|
| CPU-bound | ⚠️ Com multiprocessing | ❌ Não aplicável | ⭐⭐ Excelente | ❌ Não aplicável |
| I/O-bound (1000 conexões) | ⚠️ Consome muitos recursos | ⭐ Bom | ⭐⭐ Excelente | ⭐⭐⭐ Excelente+ |
| Simplicidade | ⭐⭐ Fácil | ⚠️ Complexo | ⭐⭐ Moderado | ⚠️ Complexo |
// ❌ Errado: Criar threads demais como faria em Python
for _ in 0..1000 {
thread::spawn(|| { /* ... */ }); // Consome muitos recursos
}
// ✅ Correto: Usar async para I/O ou thread pool para CPU
let pool = ThreadPool::new(4); // Pool com número fixo de threads
for _ in 0..1000 {
pool.execute(|| { /* ... */ });
}
.await em Funções Async// ❌ Errado: Esquecer o .await
async fn processar_dados() {
buscar_dados(); // Esqueceu .await - não executa!
}
// ✅ Correto: Usar .await
async fn processar_dados() {
buscar_dados().await; // Executa corretamente
}
// ❌ Errado: Trabalho CPU-intensive em contexto async
async fn rota_lenta() {
trabalho_pesado_cpu(); // Bloqueia o executor!
}
// ✅ Correto: Mover trabalho CPU-intensive para thread dedicado
async fn rota_lenta() {
let resultado = tokio::task::spawn_blocking(|| {
trabalho_pesado_cpu() // Executa em thread separado
}).await.unwrap();
// ... resto async
}
Rust oferece o melhor dos dois mundos: a segurança de memória que previne erros concorrentes comuns e o desempenho que permite tirar máximo proveito do hardware moderno.
Para Pythonistas, aprender Rust significa:
Em termos filosóficos, as duas linguagens representam trade-offs conscientes: Rust exige mais upfront — tipos explícitos, gerenciamento de ownership e um compilador rigoroso — para garantir segurança máxima e performance previsível. Python oferece mais facilidade inicial com tipagem dinâmica e o GIL como rede de segurança, mas esses atalhos têm um custo em performance e em race conditions que só aparecem em produção. Conhecer Rust muda a forma como você programa em qualquer linguagem.
Send/Syncrayon é a ferramenta certa para paralelismo de dados em coleções com mínima mudança de códigoA jornada de aprendizado de concorrência em Rust é recompensadora e transformará como você pensa sobre programação paralela, mesmo quando voltar ao Python.
Quer se aprofundar ainda mais? Confira o livro completo “Desbravando Rust” em desbravandorust.com.br para dominar todos esses conceitos com exemplos práticos e exercícios!