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.
Criando uma thread
Seção intitulada “Criando uma thread”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}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.
Movendo dados para dentro de uma thread
Seção intitulada “Movendo dados para dentro de uma thread”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 threadSem 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.
Compartilhando dados entre threads com segurança
Seção intitulada “Compartilhando dados entre threads com segurança”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: 5counter.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.
Prática
Seção intitulada “Prática”1. Duas threads, uma soma
Seção intitulada “1. Duas threads, uma soma”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 = 5050Essas 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.
2. Todo mundo assina o livro de visitas
Seção intitulada “2. Todo mundo assina o livro de visitas”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 aquithread 2 passou por aquithread 3 passou por aquiApague 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
movepara ficar com a posse completa dos dados que usa. - Compartilhe dados entre threads com
Arc(posse compartilhada) junto comMutex(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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Using Threads to Run Code Simultaneously — The Rust Book (em inglês):
spawn,joine closures commove, com exemplos de como a saída das threads se intercala. - Shared-State Concurrency — The Rust Book (em inglês):
MutexeArca fundo, incluindo por que umRcnão pode ser enviado para outra thread. std::thread— documentação da biblioteca padrão (em inglês): tudo sobre threads, incluindo como dar nomes a elas e as scoped threads (threads com escopo).Mutex— documentação da biblioteca padrão (em inglês): como a trava funciona e o que significa “poisoning” (envenenamento) quando uma thread entra em panic segurando a trava.- Threads — Rust by Example (em inglês): um exemplo curto e executável que cria várias threads e espera por elas.