Transferindo Dados Entre Threads com Passagem de Mensagens
Uma abordagem cada vez mais popular para garantir concorrência segura é a passagem de mensagens, em que threads ou atores se comunicam enviando mensagens contendo dados uns aos outros. Aqui está a ideia em um slogan da documentação da linguagem Go: “Não se comunique compartilhando memória; em vez disso, compartilhe memória se comunicando.”
Para realizar concorrência por passagem de mensagens, a biblioteca padrão de Rust fornece uma implementação de canais. Um canal é um conceito geral de programação pelo qual dados são enviados de uma thread para outra.
Você pode imaginar um canal em programação como um canal direcional de água, como um riacho ou um rio. Se você colocar algo como um pato de borracha em um rio, ele viajará rio abaixo até o fim do curso d’água.
Um canal tem duas metades: um transmissor e um receptor. A metade transmissora é o local rio acima onde você coloca o pato de borracha no rio, e a metade receptora é onde o pato de borracha chega rio abaixo. Uma parte do seu código chama métodos no transmissor com os dados que você quer enviar, e outra parte verifica a extremidade receptora em busca de mensagens que chegam. Dizemos que um canal está fechado se a metade transmissora ou a metade receptora for descartada.
Aqui, construiremos gradualmente um programa que tem uma thread para gerar valores e enviá-los por um canal, e outra thread que receberá os valores e os imprimirá. Enviaremos valores simples entre threads usando um canal para ilustrar o recurso. Depois que você estiver familiarizado com a técnica, poderá usar canais para quaisquer threads que precisem se comunicar entre si, como um sistema de chat ou um sistema em que muitas threads executam partes de um cálculo e enviam as partes para uma thread que agrega os resultados.
Primeiro, na Listagem 16-6, criaremos um canal, mas não faremos nada com ele. Observe que isso ainda não compila, porque Rust não consegue dizer que tipo de valores queremos enviar pelo canal.
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
tx e rxCriamos um novo canal usando a função mpsc::channel; mpsc significa
multiple producer, single consumer (múltiplos produtores, consumidor único).
Em resumo, a forma como a biblioteca padrão de Rust implementa canais significa
que um canal pode ter várias extremidades de envio que produzem valores, mas
apenas uma extremidade de recebimento que consome esses valores. Imagine
vários riachos fluindo juntos para um grande rio: tudo que for enviado por
qualquer um dos riachos terminará em um único rio no final. Começaremos com um
único produtor por enquanto, mas adicionaremos múltiplos produtores quando este
exemplo estiver funcionando.
A função mpsc::channel retorna uma tupla, cujo primeiro elemento é a
extremidade de envio, o transmissor, e cujo segundo elemento é a extremidade de
recebimento, o receptor. As abreviações tx e rx são tradicionalmente usadas
em muitos campos para transmitter e receiver, respectivamente, então damos
esses nomes às variáveis para indicar cada extremidade. Estamos usando uma
instrução let com um padrão que desestrutura a tupla; discutiremos o uso de
padrões em instruções let e desestruturação no Capítulo 19. Por enquanto,
saiba que usar uma instrução let dessa forma é uma abordagem conveniente para
extrair as partes da tupla retornada por mpsc::channel.
Vamos mover a extremidade transmissora para uma thread gerada e fazer com que ela envie uma string, para que a thread gerada se comunique com a thread principal, como mostrado na Listagem 16-7. Isso é como colocar um pato de borracha no rio, rio acima, ou enviar uma mensagem de chat de uma thread para outra.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
tx para uma thread gerada e enviando "hi"Novamente, usamos thread::spawn para criar uma nova thread e depois usamos
move para mover tx para a closure, de modo que a thread gerada tenha
ownership de tx. A thread gerada precisa ter ownership do transmissor para
poder enviar mensagens pelo canal.
O transmissor tem um método send que recebe o valor que queremos enviar. O
método send retorna um tipo Result<T, E>, então, se o receptor já tiver
sido descartado e não houver para onde enviar um valor, a operação de envio
retornará um erro. Neste exemplo, chamamos unwrap para entrar em pânico em
caso de erro. Mas, em uma aplicação real, trataríamos isso corretamente: volte
ao Capítulo 9 para revisar estratégias de tratamento de erros adequado.
Na Listagem 16-8, obteremos o valor do receptor na thread principal. Isso é como recuperar o pato de borracha da água no fim do rio ou receber uma mensagem de chat.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
"hi" na thread principal e imprimindo-oO receptor tem dois métodos úteis: recv e try_recv. Estamos usando recv,
abreviação de receive, que bloqueará a execução da thread principal e
aguardará até que um valor seja enviado pelo canal. Assim que um valor for
enviado, recv o retornará em um Result<T, E>. Quando o transmissor for
fechado, recv retornará um erro para sinalizar que nenhum outro valor virá.
O método try_recv não bloqueia; em vez disso, retorna imediatamente um
Result<T, E>: um valor Ok contendo uma mensagem, se houver alguma
disponível, e um valor Err se não houver nenhuma mensagem nesse momento. Usar
try_recv é útil se essa thread tiver outro trabalho a fazer enquanto espera
por mensagens: poderíamos escrever um loop que chama try_recv de vez em
quando, trata uma mensagem se houver uma disponível e, caso contrário, faz
outro trabalho por um tempo até verificar novamente.
Usamos recv neste exemplo por simplicidade; não temos nenhum outro trabalho
para a thread principal fazer além de esperar por mensagens, então bloquear a
thread principal é apropriado.
Quando executarmos o código da Listagem 16-8, veremos o valor impresso pela thread principal:
Got: hi
Perfeito!
Transferindo Ownership por Meio de Canais
As regras de ownership desempenham um papel vital no envio de mensagens porque
ajudam você a escrever código concorrente seguro. Prevenir erros em programação
concorrente é a vantagem de pensar em ownership em todos os seus programas
Rust. Vamos fazer um experimento para mostrar como canais e ownership trabalham
juntos para evitar problemas: tentaremos usar um valor val na thread gerada
depois de enviá-lo pelo canal. Tente compilar o código da Listagem 16-9 para
ver por que esse código não é permitido.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
val depois de enviá-lo pelo canalAqui, tentamos imprimir val depois de enviá-lo pelo canal via tx.send.
Permitir isso seria uma má ideia: depois que o valor foi enviado para outra
thread, essa thread poderia modificá-lo ou descartá-lo antes de tentarmos usar
o valor novamente. Potencialmente, as modificações da outra thread poderiam
causar erros ou resultados inesperados por causa de dados inconsistentes ou
inexistentes. No entanto, Rust nos dá um erro se tentarmos compilar o código da
Listagem 16-9:
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:27
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
Nosso erro de concorrência causou um erro em tempo de compilação. A função
send toma ownership de seu parâmetro e, quando o valor é movido, o receptor
assume ownership dele. Isso nos impede de usar acidentalmente o valor novamente
depois de enviá-lo; o sistema de ownership verifica se está tudo certo.
Enviando Vários Valores
O código da Listagem 16-8 compilou e rodou, mas não nos mostrou claramente que duas threads separadas estavam conversando entre si pelo canal.
Na Listagem 16-10, fizemos algumas modificações que deixarão claro que o código da Listagem 16-8 está rodando concorrentemente: a thread gerada agora enviará várias mensagens e fará uma pausa de um segundo entre cada mensagem.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
Desta vez, a thread gerada tem um vetor de strings que queremos enviar para a
thread principal. Iteramos sobre elas, enviando cada uma individualmente, e
pausamos entre cada envio chamando a função thread::sleep com um valor
Duration de um segundo.
Na thread principal, não chamamos mais a função recv explicitamente. Em vez
disso, tratamos rx como um iterador. Para cada valor recebido, imprimimos
esse valor. Quando o canal for fechado, a iteração terminará.
Ao executar o código da Listagem 16-10, você deve ver a seguinte saída com uma pausa de um segundo entre cada linha:
Got: hi
Got: from
Got: the
Got: thread
Como não temos nenhum código que pause ou atrase o loop for na thread
principal, podemos perceber que a thread principal está esperando para receber
valores da thread gerada.
Criando Múltiplos Produtores
Mencionamos anteriormente que mpsc é um acrônimo para multiple producer,
single consumer. Vamos colocar mpsc em uso e expandir o código da Listagem
16-10 para criar múltiplas threads que enviam valores para o mesmo receptor.
Podemos fazer isso clonando o transmissor, como mostrado na Listagem 16-11.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// --snip--
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
// --snip--
}
Desta vez, antes de criarmos a primeira thread gerada, chamamos clone no
transmissor. Isso nos dará um novo transmissor que podemos passar para a
primeira thread gerada. Passamos o transmissor original para uma segunda thread
gerada. Isso nos dá duas threads, cada uma enviando mensagens diferentes para o
mesmo receptor.
Ao executar o código, sua saída deve se parecer com esta:
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
Você pode ver os valores em outra ordem, dependendo do seu sistema. Isso é o
que torna a concorrência interessante e também difícil. Se você experimentar
com thread::sleep, fornecendo valores variados nas diferentes threads, cada
execução será mais não determinística e produzirá uma saída diferente a cada
vez.
Agora que vimos como os canais funcionam, vamos ver outro método de concorrência.