Scoped Threads em Rust: thread::scope

Guia completo de scoped threads em Rust: thread::scope, empréstimo de dados locais em threads, concorrência estruturada e processamento paralelo.

O que faz e quando usar

thread::scope (estável desde Rust 1.63) permite criar threads que podem referenciar dados da stack da thread que as criou — sem precisar de Arc, clone ou 'static. Isso é possível porque thread::scope garante que todas as threads criadas dentro do escopo terminam antes que a função retorne, implementando o conceito de concorrência estruturada (structured concurrency).

  Sem scope (thread::spawn):         Com scope (thread::scope):
  ┌─────────────────────┐            ┌──────────────────────────┐
  │ let dados = vec![..] │            │ let dados = vec![..]     │
  │                       │            │ thread::scope(|s| {      │
  │ // Precisa de Arc     │            │   s.spawn(|| &dados);   │◄─ borrow OK!
  │ // ou move             │            │   s.spawn(|| &dados);   │◄─ borrow OK!
  │ thread::spawn(move || │            │ }); // espera todas     │
  │   // owns dados        │            │ // dados ainda vivo aqui│
  └────────────────────────┘            └──────────────────────────┘

Use thread::scope quando:

  • Você quer que threads emprestem dados locais sem transferir ownership.
  • Não quer o overhead cognitivo e de performance de Arc<T>.
  • Quer garantir que todas as threads terminam antes de continuar (structured concurrency).
  • Está fazendo processamento paralelo de dados em uma fatia (slice) ou vetor.

Tipos e Funções Principais

ItemDescrição
thread::scope(f)Cria um escopo; f recebe um &Scope
scope.spawn(f)Cria uma thread dentro do escopo
ScopedJoinHandle<T>Handle para o resultado da thread escopo
handle.join()Aguarda a thread terminar (chamado automaticamente no fim do escopo)

Importante: Ao final de thread::scope, todas as threads são automaticamente joined. Se qualquer thread entrar em panic, o panic é propagado para a thread chamadora.


Exemplos de Código

Empréstimo básico de dados da stack

use std::thread;

fn main() {
    let numeros = vec![1, 2, 3, 4, 5];
    let mensagem = String::from("processando");

    // thread::scope garante que as threads terminam antes de retornar
    thread::scope(|s| {
        // Esta thread pode emprestar &numeros — sem Arc, sem move!
        s.spawn(|| {
            let soma: i32 = numeros.iter().sum();
            println!("{}: soma = {}", mensagem, soma);
        });

        // Outra thread também pode emprestar os mesmos dados
        s.spawn(|| {
            let max = numeros.iter().max().unwrap();
            println!("{}: máximo = {}", mensagem, max);
        });
    }); // Todas as threads são joined aqui automaticamente

    // numeros e mensagem ainda estão disponíveis!
    println!("Dados originais: {:?}", numeros);
    println!("Mensagem: {}", mensagem);
}

Comparação: spawn vs scope

use std::sync::Arc;
use std::thread;

fn main() {
    let dados = vec![10, 20, 30, 40, 50];

    // COM thread::spawn — precisa de Arc + clone
    {
        let dados = Arc::new(dados.clone());

        let d1 = Arc::clone(&dados);
        let h1 = thread::spawn(move || {
            println!("spawn: soma = {}", d1.iter().sum::<i32>());
        });

        let d2 = Arc::clone(&dados);
        let h2 = thread::spawn(move || {
            println!("spawn: len = {}", d2.len());
        });

        h1.join().unwrap();
        h2.join().unwrap();
    }

    // COM thread::scope — borrow direto, sem Arc
    {
        thread::scope(|s| {
            s.spawn(|| {
                println!("scope: soma = {}", dados.iter().sum::<i32>());
            });

            s.spawn(|| {
                println!("scope: len = {}", dados.len());
            });
        });
    }

    // Muito mais simples e eficiente!
}

Escrita concorrente com fatias mutáveis

Uma das maiores vantagens de scoped threads: dividir um slice mutável entre threads sem locks:

use std::thread;

fn main() {
    let mut dados = vec![0u64; 12];

    // Dividir o vetor em fatias mutáveis — cada thread escreve na sua fatia
    thread::scope(|s| {
        for (i, chunk) in dados.chunks_mut(3).enumerate() {
            s.spawn(move || {
                for (j, valor) in chunk.iter_mut().enumerate() {
                    *valor = (i * 3 + j) as u64 * 10;
                }
                println!("Thread {} preencheu {:?}", i, chunk);
            });
        }
    });

    println!("Resultado: {:?}", dados);
    // [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110]
}

Processamento paralelo de dados

use std::thread;

fn processar_item(item: &str) -> usize {
    // Simular processamento custoso
    std::thread::sleep(std::time::Duration::from_millis(50));
    item.len()
}

fn main() {
    let itens = vec![
        "Rust", "Brasil", "Concorrência", "Segurança",
        "Performance", "Threads", "Escopo", "Paralelo",
    ];

    let resultados: Vec<usize> = thread::scope(|s| {
        let handles: Vec<_> = itens
            .iter()
            .map(|item| {
                s.spawn(move || processar_item(item))
            })
            .collect();

        handles
            .into_iter()
            .map(|h| h.join().unwrap())
            .collect()
    });

    for (item, resultado) in itens.iter().zip(resultados.iter()) {
        println!("{}: {} chars", item, resultado);
    }
}

Acessando múltiplas referências

use std::thread;

struct DadosApp {
    usuarios: Vec<String>,
    config: Config,
}

struct Config {
    max_threads: usize,
    timeout_ms: u64,
}

fn main() {
    let app = DadosApp {
        usuarios: vec![
            "Alice".into(), "Bruno".into(), "Carla".into(),
        ],
        config: Config {
            max_threads: 4,
            timeout_ms: 5000,
        },
    };

    thread::scope(|s| {
        // Thread 1: lê usuários
        s.spawn(|| {
            for user in &app.usuarios {
                println!("Usuário: {}", user);
            }
        });

        // Thread 2: lê config (pode ler ao mesmo tempo — é referência imutável)
        s.spawn(|| {
            println!(
                "Config: max_threads={}, timeout={}ms",
                app.config.max_threads, app.config.timeout_ms
            );
        });
    });

    // app ainda disponível
    println!("Total de usuários: {}", app.usuarios.len());
}

Retornando valores de scoped threads

use std::thread;

fn main() {
    let dados = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // scope pode retornar um valor
    let (soma, produto) = thread::scope(|s| {
        let h_soma = s.spawn(|| -> i64 {
            dados.iter().map(|x| *x as i64).sum()
        });

        let h_produto = s.spawn(|| -> i64 {
            dados.iter().map(|x| *x as i64).product()
        });

        let soma = h_soma.join().unwrap();
        let produto = h_produto.join().unwrap();
        (soma, produto)
    });

    println!("Soma: {}, Produto: {}", soma, produto);
    // Soma: 55, Produto: 3628800
}

Tratando panics em scoped threads

use std::thread;

fn main() {
    let resultado = std::panic::catch_unwind(|| {
        thread::scope(|s| {
            s.spawn(|| {
                println!("Thread normal executando");
            });

            s.spawn(|| {
                panic!("Ops! Thread em pânico!");
            });

            s.spawn(|| {
                println!("Outra thread executando");
            });
        });
        // Se qualquer thread entrou em panic, scope propaga o panic aqui
    });

    match resultado {
        Ok(()) => println!("Todas as threads completaram com sucesso"),
        Err(_) => println!("Pelo menos uma thread entrou em pânico!"),
    }
}

Padrões Comuns e Anti-padrões

Padrão: map-reduce paralelo com scope

use std::thread;

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

    let soma_total: u64 = thread::scope(|s| {
        let handles: Vec<_> = dados
            .chunks(chunk_size)
            .map(|chunk| {
                s.spawn(|| -> u64 {
                    chunk.iter().sum() // borrow direto do chunk!
                })
            })
            .collect();

        handles
            .into_iter()
            .map(|h| h.join().unwrap())
            .sum()
    });

    println!("Soma: {}", soma_total);
    // Soma: 500000500000
}

Anti-padrão: usar scope onde spawn seria melhor

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

fn main() {
    // ERRADO: usar scope para threads de longa duração que não precisam
    // acessar dados locais — scope bloqueia até todas terminarem!
    //
    // thread::scope(|s| {
    //     s.spawn(|| {
    //         loop { /* servidor rodando para sempre */ }
    //     });
    // }); // Nunca retorna!

    // CORRETO: usar spawn para threads independentes de longa duração
    let _handle = thread::spawn(|| {
        // Background worker
        thread::sleep(Duration::from_secs(1));
    });

    // CORRETO: usar scope para trabalho paralelo de curta duração
    let dados = vec![1, 2, 3, 4, 5];
    thread::scope(|s| {
        for chunk in dados.chunks(2) {
            s.spawn(|| {
                println!("Processando: {:?}", chunk);
            });
        }
    });
}

Anti-padrão: tentar mover dados para fora do scope

use std::thread;

fn main() {
    let dados = vec![1, 2, 3];

    thread::scope(|s| {
        // ERRO: não pode mover dados para dentro de uma scoped thread
        // se o dado é emprestado por outra thread no mesmo escopo

        // Isso funciona (apenas uma thread usa o dado com move):
        // s.spawn(move || {
        //     println!("{:?}", dados);
        // });

        // Isso funciona (múltiplas threads com referência):
        s.spawn(|| {
            println!("Thread 1: {:?}", &dados);
        });
        s.spawn(|| {
            println!("Thread 2: {:?}", &dados);
        });
    });

    println!("Ainda disponível: {:?}", dados);
}

Garantias de Thread Safety

  • thread::scope garante que todas as threads criadas terminam antes de retornar (structured concurrency).
  • Threads dentro do scope podem emprestar dados com lifetimes menores que 'static — diferente de thread::spawn.
  • Se qualquer thread entrar em panic, scope aguarda as demais e então propaga o panic.
  • chunks_mut() com scoped threads é a forma segura de escrever em fatias diferentes de um vetor em paralelo, sem locks.
  • A closure passada para scope.spawn() precisa ser Send (os dados capturados devem ser transferíveis entre threads).

Veja Também