std::thread em Rust: Criando Threads

Guia completo de std::thread em Rust: spawn, JoinHandle, Builder, move closures, sleep, yield_now e current com exemplos práticos em português.

O que faz e quando usar

O módulo std::thread fornece a API para criar e gerenciar threads nativas do sistema operacional em Rust. Cada thread criada com thread::spawn corresponde a uma thread real do SO (1:1 threading), com sua própria pilha de execução (stack). Threads são a forma mais direta de alcançar paralelismo em Rust — executar código simultaneamente em múltiplos núcleos da CPU.

Use std::thread quando:

  • Você precisa executar trabalho CPU-bound em paralelo (cálculos, processamento de dados).
  • Quer executar operações bloqueantes sem travar a thread principal.
  • Precisa de paralelismo verdadeiro (não apenas concorrência cooperativa como async/await).
  • Quer compartilhar dados entre tarefas usando Arc<Mutex<T>> ou canais mpsc.

Evite criar threads para cada requisição de I/O — para I/O-bound, considere async/await com Tokio. Cada thread consome ~8KB de stack por padrão, então milhares de threads podem esgotar memória.


Tipos e Funções Principais

ItemDescrição
thread::spawn(f)Cria uma nova thread executando a closure f
JoinHandle<T>Handle para aguardar a thread e obter seu resultado
JoinHandle::join()Bloqueia até a thread terminar, retorna Result<T>
thread::sleep(dur)Suspende a thread atual por pelo menos dur
thread::yield_now()Cede o processador para outra thread
thread::current()Retorna um Thread representando a thread atual
thread::BuilderConstrutor para configurar nome e tamanho de stack
thread::park()Suspende a thread até ser “desparkada”
thread::Thread::unpark()Acorda uma thread suspensa com park()

Exemplos de Código

Criando uma thread básica com spawn

A função thread::spawn recebe uma closure e retorna um JoinHandle:

use std::thread;
use std::time::Duration;

fn main() {
    // Criar uma nova thread
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("[filha] contagem: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
        42 // valor de retorno da thread
    });

    // A thread principal continua executando em paralelo
    for i in 1..=3 {
        println!("[principal] contagem: {}", i);
        thread::sleep(Duration::from_millis(150));
    }

    // join() bloqueia até a thread terminar
    // Retorna Result<T, Box<dyn Any + Send>>
    let resultado = handle.join().expect("Thread entrou em pânico");
    println!("Thread filha retornou: {}", resultado);
}

Move closures — transferindo ownership

Quando a closure precisa usar dados da thread que a criou, use move para transferir a posse:

use std::thread;

fn main() {
    let dados = vec![10, 20, 30, 40, 50];
    let nome = String::from("processamento");

    // `move` transfere ownership de `dados` e `nome`
    let handle = thread::spawn(move || {
        println!("Tarefa: {}", nome);
        let soma: i32 = dados.iter().sum();
        println!("Soma: {}", soma);
        soma
    });

    // dados e nome NÃO estão mais disponíveis aqui!
    // println!("{}", nome); // ERRO: value borrowed here after move

    let resultado = handle.join().unwrap();
    println!("Resultado: {}", resultado);
}

Erro comum — esquecer move:

use std::thread;

fn main() {
    let mensagem = String::from("olá");

    // ERRO DE COMPILAÇÃO: closure may outlive the current function
    // let handle = thread::spawn(|| {
    //     println!("{}", mensagem);
    // });

    // Correto: usar move
    let handle = thread::spawn(move || {
        println!("{}", mensagem);
    });

    handle.join().unwrap();
}

O compilador exige move porque a thread criada pode viver mais que o escopo onde mensagem foi declarada.

Builder — configurando nome e stack

O thread::Builder permite personalizar a thread antes de criá-la:

use std::thread;

fn main() {
    let builder = thread::Builder::new()
        .name("worker-01".into())   // nome da thread (aparece em logs/debug)
        .stack_size(4 * 1024 * 1024); // stack de 4MB (padrão: ~8MB)

    let handle = builder.spawn(|| {
        let thread_atual = thread::current();
        println!(
            "Executando na thread: {:?}",
            thread_atual.name().unwrap_or("sem nome")
        );

        // Trabalho pesado que pode precisar de mais stack
        let resultado = calcular_recursivo(20);
        println!("Resultado: {}", resultado);
        resultado
    }).expect("Falha ao criar thread");

    let valor = handle.join().unwrap();
    println!("Thread nomeada retornou: {}", valor);
}

fn calcular_recursivo(n: u64) -> u64 {
    if n <= 1 { return n; }
    calcular_recursivo(n - 1) + calcular_recursivo(n - 2)
}

thread::sleep e thread::yield_now

use std::thread;
use std::time::{Duration, Instant};

fn main() {
    // sleep: suspende por pelo menos a duração especificada
    let inicio = Instant::now();
    thread::sleep(Duration::from_millis(100));
    println!("sleep durou: {:?}", inicio.elapsed());

    // yield_now: cede o processador voluntariamente
    // Útil em loops apertados para dar chance a outras threads
    let handle = thread::spawn(|| {
        for i in 0..5 {
            println!("Trabalhando... {}", i);
            thread::yield_now(); // dá chance a outras threads
        }
    });

    handle.join().unwrap();
}

thread::current — identificando a thread

use std::thread;

fn quem_sou_eu() {
    let atual = thread::current();
    println!(
        "Thread: {:?}, ID: {:?}",
        atual.name().unwrap_or("anônima"),
        atual.id()
    );
}

fn main() {
    quem_sou_eu(); // thread principal

    let handle = thread::Builder::new()
        .name("minha-thread".into())
        .spawn(quem_sou_eu)
        .unwrap();

    handle.join().unwrap();
}

Criando múltiplas threads e coletando resultados

use std::thread;

fn main() {
    let dados = vec![
        vec![1, 2, 3, 4, 5],
        vec![10, 20, 30],
        vec![100, 200, 300, 400],
    ];

    // Criar uma thread para processar cada vetor
    let handles: Vec<_> = dados
        .into_iter()
        .enumerate()
        .map(|(id, chunk)| {
            thread::spawn(move || {
                let soma: i32 = chunk.iter().sum();
                println!("Thread {}: soma = {}", id, soma);
                soma
            })
        })
        .collect();

    // Coletar todos os resultados
    let resultados: Vec<i32> = handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .collect();

    let total: i32 = resultados.iter().sum();
    println!("Total de todas as threads: {}", total);
}

Tratando panics em threads

Quando uma thread entra em pânico, o panic fica contido naquela thread. join() retorna Err se a thread deu panic:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        panic!("Algo deu muito errado!");
    });

    // O panic NÃO se propaga para a thread principal automaticamente
    match handle.join() {
        Ok(valor) => println!("Thread retornou: {:?}", valor),
        Err(panic_info) => {
            // Tentar extrair a mensagem de panic
            if let Some(msg) = panic_info.downcast_ref::<&str>() {
                eprintln!("Thread entrou em pânico: {}", msg);
            } else if let Some(msg) = panic_info.downcast_ref::<String>() {
                eprintln!("Thread entrou em pânico: {}", msg);
            } else {
                eprintln!("Thread entrou em pânico com valor desconhecido");
            }
        }
    }

    println!("Thread principal continua normalmente.");
}

Padrões Comuns

Padrão: dividir trabalho entre N threads

  Thread Principal
        |
   +----|----+----+
   |    |    |    |
  [T0] [T1] [T2] [T3]   <-- cada thread processa parte dos dados
   |    |    |    |
   +----|----+----+
        |
   Coleta resultados
use std::thread;

fn main() {
    let dados: Vec<u64> = (1..=1_000_000).collect();
    let num_threads = 4;
    let tamanho_chunk = dados.len() / num_threads;

    let handles: Vec<_> = dados
        .chunks(tamanho_chunk)
        .map(|chunk| {
            let chunk = chunk.to_vec(); // clonar para ownership
            thread::spawn(move || -> u64 {
                chunk.iter().sum()
            })
        })
        .collect();

    let total: u64 = handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .sum();

    println!("Soma de 1 a 1.000.000: {}", total);
    // Soma de 1 a 1.000.000: 500000500000
}

Anti-padrão: criar threads demais

// RUIM: uma thread por item — overhead enorme!
// fn processar_ruim(itens: Vec<String>) {
//     let handles: Vec<_> = itens.into_iter().map(|item| {
//         thread::spawn(move || { /* trabalho mínimo */ })
//     }).collect();
// }

// BOM: dividir em chunks proporcionais ao número de CPUs
fn processar_bom(itens: Vec<String>) {
    let num_cpus = thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(4);

    let chunk_size = (itens.len() + num_cpus - 1) / num_cpus;

    let handles: Vec<_> = itens
        .chunks(chunk_size)
        .map(|chunk| {
            let chunk = chunk.to_vec();
            std::thread::spawn(move || {
                for item in &chunk {
                    println!("Processando: {}", item);
                }
            })
        })
        .collect();

    for h in handles {
        h.join().unwrap();
    }
}

fn main() {
    let itens: Vec<String> = (0..100).map(|i| format!("item-{}", i)).collect();
    processar_bom(itens);
}

Garantias de Thread Safety

  • thread::spawn exige que a closure seja Send + 'static. Isso significa que todos os valores capturados devem ser transferíveis entre threads (Send) e não podem conter referências com lifetime limitado ('static).
  • Se você precisa compartilhar referências sem 'static, use thread::scope (Rust 1.63+).
  • Panics em threads filhas são isolados — não derrubam a thread principal a menos que você propague com join().unwrap().
  • O JoinHandle implementa Send, então pode ser transferido entre threads.

Veja Também