Pular para o conteúdo

Concorrência em Rust

Concorrência, ou seja, executar várias tarefas ao mesmo tempo, é onde muitas linguagens ficam perigosas de verdade. Duas threads mexendo nos mesmos dados ao mesmo tempo podem corrompê-los de jeitos muito difíceis de rastrear. Esse é exatamente o tipo de bug que as regras de borrowing (empréstimo) foram criadas para evitar, e essa proteção vale diretamente para threads também.

use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=3 {
println!("thread: {i}");
}
});
for i in 1..=3 {
println!("main: {i}");
}
handle.join().unwrap(); // espera a thread terminar
}
▶ Experimente você mesmo

thread::spawn recebe uma closure (função anônima) e a executa em uma nova thread, em paralelo com todo o resto. .join() fica esperando até essa thread terminar. Sem ele, o main poderia acabar antes mesmo de a thread nova ter a chance de rodar.

Uma closure passada para thread::spawn precisa ser dona de todos os dados que usa, porque ela pode rodar depois que o escopo original já terminou. Use a palavra-chave move para garantir isso:

use std::thread;
fn main() {
let message = String::from("olá de uma thread");
let handle = thread::spawn(move || {
println!("{message}");
});
handle.join().unwrap();
}

Saída:

olá de uma thread
▶ Experimente você mesmo

Sem o move, o Rust não compila esse código: ele não tem como garantir que message ainda vai ser válida quando a nova thread finalmente usá-la. O move transfere a posse de message para dentro da closure. É o mesmo “move” que você viu na página de Ownership, só que entregue a uma thread em vez de a outra variável.

Referências comuns não funcionam entre threads (elas não podem viver mais que o escopo que as criou). Por isso, compartilhar dados entre threads junta duas coisas que você já conhece: o Arc (da página de Ponteiros inteligentes) para a posse compartilhada, e um Mutex para garantir que só uma thread mexa nos dados de cada vez:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *counter.lock().unwrap());
}

Saída:

Resultado: 5
▶ Experimente você mesmo

counter.lock() espera até conseguir acesso exclusivo ao valor com segurança, faz a atualização e depois libera o acesso automaticamente. Cinco threads incrementam o mesmo contador e, graças ao Mutex, uma nunca atropela a outra: o resultado é sempre 5, e não um número menor por causa de uma atualização perdida.

Travar dados compartilhados é um jeito de as threads cooperarem. O outro jeito comum é não compartilhar nada e fazer as threads mandarem mensagens umas para as outras. É disso que trata a página de Canais (channels).

Por que o Rust chama isso de “concorrência sem medo”

Seção intitulada “Por que o Rust chama isso de “concorrência sem medo””

Na maioria das linguagens, nada impede que você esqueça de travar um Mutex ou que, sem querer, compartilhe entre threads uma referência que não deveria ser compartilhada. O compilador do Rust realmente cobra isso: um código que causaria uma data race (condição de corrida nos dados) entre threads simplesmente não compila. É o mesmo sistema de ownership e borrowing que você aprendeu antes neste curso, só que aplicado a threads em vez de a um único escopo. Em inglês, isso é chamado de fearless concurrency.

Some os números de 1 a 100 usando duas threads: uma soma de 1 a 50, e a outra de 51 a 100. Depois o main junta as duas respostas. Dica: a closure de uma thread pode retornar um valor, e handle.join().unwrap() devolve esse valor para você.

Ver solução
use std::thread;
fn main() {
let first = thread::spawn(|| {
let total: u32 = (1..=50).sum();
total
});
let second = thread::spawn(|| {
let total: u32 = (51..=100).sum();
total
});
let a = first.join().unwrap();
let b = second.join().unwrap();
println!("{a} + {b} = {}", a + b);
}

Saída:

1275 + 3775 = 5050
▶ Experimente você mesmo

Essas closures não usam nenhuma variável de fora, então não precisam de move. As duas threads rodam ao mesmo tempo, e o join() espera cada uma terminar e devolve o resultado dela.

Crie três threads. Cada uma acrescenta uma linha como "thread 2 passou por aqui" a um único Vec<String> compartilhado. Depois que todas as threads terminarem, imprima as linhas em ordem. Dica: use Arc<Mutex<Vec<String>>>, como no exemplo do contador, e ordene uma cópia no final, porque as threads podem terminar em qualquer ordem.

Ver solução
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let book = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for id in 1..=3 {
let book = Arc::clone(&book);
handles.push(thread::spawn(move || {
book.lock().unwrap().push(format!("thread {id} passou por aqui"));
}));
}
for handle in handles {
handle.join().unwrap();
}
let mut lines = book.lock().unwrap().clone();
lines.sort();
for line in lines {
println!("{line}");
}
}

Saída:

thread 1 passou por aqui
thread 2 passou por aqui
thread 3 passou por aqui
▶ Experimente você mesmo

Apague a linha lines.sort() e rode algumas vezes: a ordem pode mudar. Ordenar é o jeito de ter a mesma saída toda vez.

  • thread::spawn(closure) executa uma closure em uma nova thread; .join() espera ela terminar.
  • Uma closure passada para uma thread geralmente precisa de move para ficar com a posse completa dos dados que usa.
  • Compartilhe dados entre threads com Arc (posse compartilhada) junto com Mutex (acesso exclusivo): counter.lock() dá acesso seguro ao valor, uma thread de cada vez.
  • O compilador rejeita, na hora de compilar, um código que causaria uma data race, em vez de deixar o problema aparecer com o programa rodando.

Teste o que aprendeu

1. Por que uma closure passada para thread::spawn geralmente precisa da palavra-chave move?

2. Para que serve Arc<Mutex<T>>?

3. O que pode dar errado se você esquecer de chamar .join() no handle de uma thread?

4. Por que a ordem das linhas "main:" e "thread:" pode mudar entre execuções do mesmo programa?

Pontuação: 0 / 4