Rust

Channels em Rust: Comunicação entre Threads com mpsc na Prática

8 min de leitura

Channels em Rust: Comunicação entre Threads com mpsc na Prática

Entendendo Channels e o Padrão MPSC Channels (canais) em Rust são primitivas de sincronização que implementam o padrão MPSC (Multiple Producer, Single Consumer). Isso significa que múltiplas threads podem enviar mensagens através de um canal, mas apenas uma thread pode receber. Esse padrão evita condições de corrida e garante segurança de memória — características fundamentais do Rust. O channel em Rust é composto por dois extremos: um sender (transmissor) e um receiver (receptor). Quando você cria um channel, ambos são retornados juntos. O sender pode ser clonado para múltiplas threads produzirem dados, enquanto o receiver permanece singular. Essa restrição não é uma limitação, mas sim um design que força você a pensar corretamente sobre concorrência. Por que não usar mutex? Um mutex protege dados compartilhados, mas não coordena a comunicação entre threads. Channels, por outro lado, são ideais quando você precisa que uma thread notifique outra sobre novos dados. Mutex = compartilhamento de estado; Channel = passagem de mensagens. Criando

<h2>Entendendo Channels e o Padrão MPSC</h2>

<p>Channels (canais) em Rust são primitivas de sincronização que implementam o padrão <strong>MPSC</strong> (Multiple Producer, Single Consumer). Isso significa que múltiplas threads podem enviar mensagens através de um canal, mas apenas uma thread pode receber. Esse padrão evita condições de corrida e garante segurança de memória — características fundamentais do Rust.</p>

<p>O channel em Rust é composto por dois extremos: um <strong>sender</strong> (transmissor) e um <strong>receiver</strong> (receptor). Quando você cria um channel, ambos são retornados juntos. O sender pode ser clonado para múltiplas threads produzirem dados, enquanto o receiver permanece singular. Essa restrição não é uma limitação, mas sim um design que força você a pensar corretamente sobre concorrência.</p>

<h3>Por que não usar mutex?</h3>

<p>Um mutex protege dados compartilhados, mas não coordena a comunicação entre threads. Channels, por outro lado, são ideais quando você precisa que uma thread <strong>notifique</strong> outra sobre novos dados. Mutex = compartilhamento de estado; Channel = passagem de mensagens.</p>

<h2>Criando e Usando Channels Básicos</h2>

<p>A criação de um channel é simples através da função <code>mpsc::channel()</code>. Vejamos um exemplo prático:</p>

<pre><code class="language-rust">use std::sync::mpsc;

use std::thread;

fn main() {

// Cria um novo channel MPSC

let (tx, rx) = mpsc::channel();

// Thread produtora

thread::spawn(move || {

let mensagem = String::from(&quot;Olá da thread!&quot;);

tx.send(mensagem).unwrap();

});

// Thread principal recebe

let valor_recebido = rx.recv().unwrap();

println!(&quot;Recebi: {}&quot;, valor_recebido);

}</code></pre>

<p>No código acima, <code>tx</code> é o transmissor (sender) e <code>rx</code> é o receptor. O <code>move</code> garante que o ownership de <code>tx</code> seja transferido para a closure. O método <code>send()</code> envia um valor e retorna um <code>Result</code>, que deve ser tratado. Se o receptor foi droppado, <code>send()</code> retorna erro.</p>

<h3>Enviando Múltiplas Mensagens</h3>

<p>Frequentemente você precisa enviar vários dados sequenciais. Use um loop no produtor:</p>

<pre><code class="language-rust">use std::sync::mpsc;

use std::thread;

use std::time::Duration;

fn main() {

let (tx, rx) = mpsc::channel();

thread::spawn(move || {

let dados = vec![&quot;Mensagem 1&quot;, &quot;Mensagem 2&quot;, &quot;Mensagem 3&quot;];

for msg in dados {

tx.send(msg).unwrap();

thread::sleep(Duration::from_millis(500));

}

});

// Itera sobre todas as mensagens até o canal fechar

for valor in rx {

println!(&quot;Recebido: {}&quot;, valor);

}

}</code></pre>

<p>Aqui, o receptor implementa <code>IntoIterator</code>, permitindo iteração direta. Quando o sender é droppado, a iteração termina automaticamente.</p>

<h2>Canais com Múltiplos Produtores</h2>

<p>O verdadeiro poder do MPSC emerge quando múltiplas threads produzem simultaneamente. Clone o sender para cada produtor:</p>

<pre><code class="language-rust">use std::sync::mpsc;

use std::thread;

fn main() {

let (tx, rx) = mpsc::channel();

// Produtor 1

let tx1 = tx.clone();

thread::spawn(move || {

tx1.send(&quot;Dados da thread 1&quot;).unwrap();

});

// Produtor 2

let tx2 = tx.clone();

thread::spawn(move || {

tx2.send(&quot;Dados da thread 2&quot;).unwrap();

});

// Não esqueça de dropar o transmissor original

drop(tx);

// Receptor consome ambas as mensagens

for valor in rx {

println!(&quot;Recebido: {}&quot;, valor);

}

}</code></pre>

<blockquote><p><strong>Detalhe crítico</strong>: Você deve dropar o sender original após clonar. Se deixar <code>tx</code> vivo, o receptor nunca saberá quando parar de esperar, pois ainda há um produtor ativo.</p></blockquote>

<h2>Canais Bounded vs Unbounded</h2>

<p>Por padrão, <code>mpsc::channel()</code> cria um <strong>unbounded</strong> (ilimitado) — a fila interna cresce indefinidamente. Para controlar memória, use <code>mpsc::sync_channel(n)</code> que limita o buffer a <code>n</code> mensagens:</p>

<pre><code class="language-rust">use std::sync::mpsc;

use std::thread;

fn main() {

let (tx, rx) = mpsc::sync_channel(2); // Buffer de 2 mensagens

thread::spawn(move || {

tx.send(1).unwrap(); // Ok

tx.send(2).unwrap(); // Ok

tx.send(3).unwrap(); // Bloqueia até algo ser recebido!

});

thread::sleep(std::time::Duration::from_secs(1));

println!(&quot;Recebido: {}&quot;, rx.recv().unwrap());

}</code></pre>

<p>Com <code>sync_channel(2)</code>, o terceiro <code>send()</code> bloqueia porque o buffer está cheio. Isso oferece <strong>backpressure</strong> automática — produtores rápidos são freados se o receptor não acompanhar.</p>

<h2>Tratamento de Erros com Channels</h2>

<p>Cometa um erro comum: ignorar <code>Result</code>. Aqui está o tratamento correto:</p>

<pre><code class="language-rust">use std::sync::mpsc;

use std::thread;

fn main() {

let (tx, rx) = mpsc::channel();

thread::spawn(move || {

match tx.send(&quot;Olá&quot;) {

Ok(_) =&gt; println!(&quot;Mensagem enviada com sucesso&quot;),

Err(_) =&gt; println!(&quot;Receptor foi droppado!&quot;),

}

});

// Se o receptor é droppado antes de receber

drop(rx);

thread::sleep(std::time::Duration::from_millis(100));

}</code></pre>

<p><code>send()</code> retorna <code>Err</code> apenas se o receptor foi droppado. Para o lado receptor, <code>recv()</code> retorna <code>Err</code> se <strong>todos</strong> os senders foram droppados. Use <code>try_recv()</code> para não-blocking:</p>

<pre><code class="language-rust">match rx.try_recv() {

Ok(msg) =&gt; println!(&quot;Mensagem: {}&quot;, msg),

Err(mpsc::TryRecvError::Empty) =&gt; println!(&quot;Nada aguardando&quot;),

Err(mpsc::TryRecvError::Disconnected) =&gt; println!(&quot;Canal fechado&quot;),

}</code></pre>

<h2>Conclusão</h2>

<p><strong>MPSC channels em Rust são a ferramenta ideal para comunicação entre threads</strong> porque combinam segurança, performance e clareza de intenção. Três aprendizados principais:</p>

<ol>

<li><strong>Channels garantem segurança sem deadlocks</strong> — o padrão MPSC força uma arquitetura saudável onde produtores e consumidores têm papéis bem definidos.</li>

</ol>

<ol>

<li><strong>Clone o sender, nunca o receiver</strong> — essa assimetria é proposital e evita condições de corrida na sincronização.</li>

</ol>

<ol>

<li><strong>Sempre trate erros e gerencie lifetimes</strong> — dropar senders explicitamente e usar <code>sync_channel</code> para backpressure são detalhes que separam código robusto de código frágil.</li>

</ol>

<p>Para sistemas concorrentes em Rust, channels são superiores a mutex compartilhado. Use-os como primeira opção em comunicação inter-thread.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://doc.rust-lang.org/std/sync/mpsc/" target="_blank" rel="noopener noreferrer">Rust Official Documentation - std::sync::mpsc</a></li>

<li><a href="https://doc.rust-lang.org/book/ch16-02-message-passing.html" target="_blank" rel="noopener noreferrer">The Rust Book - Fearless Concurrency</a></li>

<li><a href="https://doc.rust-lang.org/rust-by-example/std_misc/channels.html" target="_blank" rel="noopener noreferrer">Rust by Example - Channels</a></li>

<li><a href="https://tokio.rs/tokio/tutorial/select" target="_blank" rel="noopener noreferrer">Tokio - Async Channels Guide</a></li>

</ul>

Comentários

Mais em Rust

O que Todo Dev Deve Saber sobre HashMap e HashSet em Rust: Estruturas de Dados por Chave
O que Todo Dev Deve Saber sobre HashMap e HashSet em Rust: Estruturas de Dados por Chave

HashMap: Armazenamento Eficiente com Chaves HashMap é uma estrutura de dados...

Como Usar Workspaces no Cargo: Organizando Projetos Grandes em Rust em Produção
Como Usar Workspaces no Cargo: Organizando Projetos Grandes em Rust em Produção

O que são Workspaces no Cargo? Um workspace no Cargo é um mecanismo para orga...

O que Todo Dev Deve Saber sobre Benchmarking e Profiling de Performance em Rust
O que Todo Dev Deve Saber sobre Benchmarking e Profiling de Performance em Rust

Profiling: Medindo o Tempo de Execução Profiling é o processo de medir onde s...