Channel | CSharp
- Channel | CSharp
Implementando o Padrão Produtor/Consumidor com Channel em C#
É importante lembrar que ao definir uma fila que suporta 10 mil mensagens, é interessante validar a memória da máquina.
Contexto: Uma máquina com 2GB RAM, aceitando 10 mil mensagens no channel e cada mensagem trazia dados extensos de consultas SQL.
Consequência: Máquina topando seus 90% de uso de memória
Se parece com mensagem, mas interna. FICA NA INFRAESTRUTURA: Porque ele é um detalhe técnico de processamento assíncrono / fila interna / threading, não regra de negócio. Possui interface no Application ou Domain.
Os canais (Channels) em C# são uma forma eficiente de comunicação assíncrona baseada no conceito de passagem de mensagens. Eles permitem que um produtor envie mensagens para um canal, onde ficam armazenadas até que um consumidor esteja pronto para recebê-las. Isso facilita a coordenação entre diferentes partes de um programa, especialmente em cenários de processamento concorrente.
O que é um Channel?
Os Channels fazem parte do namespace System.Threading.Channels e permitem a comunicação segura entre múltiplas threads. Eles podem ser usados para criar pipelines eficientes, reduzindo a necessidade de bloqueios explícitos.
Os principais tipos de Channels disponíveis são:
- UnboundedChannel: Possui capacidade ilimitada, podendo armazenar um número arbitrário de mensagens.
- BoundedChannel: Possui um limite máximo de mensagens. Quando atinge essa capacidade, o comportamento pode ser configurado com
BoundedChannelOptions.FullMode, permitindo descartar mensagens ou bloquear o produtor até que haja espaço disponível.
Além disso, algumas propriedades úteis para otimização incluem:
- SingleWriter: Restringe o canal a um único produtor, permitindo otimizações internas.
- SingleReader: Restringe o canal a um único consumidor.
- AllowSynchronousContinuations: Permite que operações sejam continuadas de forma síncrona, caso necessário.
Criando e Usando um Channel
1. Criando um Channel
Para criar um Channel, usamos o método estático do tipo de canal desejado:
var channel = Channel.CreateUnbounded<int>();
Esse código cria um Channel ilimitado, onde mensagens do tipo int podem ser enviadas e recebidas.
2. Enviando e Recebendo Mensagens
Após criar o Channel, podemos enviar e receber mensagens da seguinte forma:
// Escrita (Produtor)
await channel.Writer.WriteAsync(42);
// Leitura (Consumidor)
int message = await channel.Reader.ReadAsync();
Console.WriteLine($"Mensagem recebida: {message}");
3. Uso em Ambiente Multithread
Os Channels são extremamente úteis para sincronização em cenários multithread. O exemplo abaixo demonstra um produtor e um consumidor rodando em threads separadas:
var channel = Channel.CreateUnbounded<int>();
// Thread do produtor
Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
Console.WriteLine($"Produziu: {i}");
await Task.Delay(500); // Simula trabalho
}
channel.Writer.Complete(); // Indica que não há mais mensagens
});
// Thread do consumidor
Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consumiu: {item}");
}
});
Aqui, o produtor escreve valores no Channel, enquanto o consumidor os processa de forma assíncrona.
Tipos de Channels e Estratégias de Uso
1. UnboundedChannel (Canal Ilimitado)
O UnboundedChannel é útil quando não sabemos quantas mensagens serão processadas e quando o consumidor pode acompanhar a produção.
Exemplo:
var channel = Channel.CreateUnbounded<int>();
// Produzindo mensagens
Task.Run(async () =>
{
for (int i = 0; i < 100; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
// Consumindo mensagens
Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Recebido: {item}");
}
});
Atenção: Se o produtor for muito mais rápido que o consumidor, a memória pode ser consumida rapidamente.
2. BoundedChannel (Canal Limitado)
O BoundedChannel permite definir um limite máximo de mensagens, evitando consumo excessivo de memória.
Exemplo:
var options = new BoundedChannelOptions(10)
{
FullMode = BoundedChannelFullMode.DropNewest // Estratégia de descarte ao atingir limite
};
var channel = Channel.CreateBounded<int>(options);
Aqui, quando o buffer estiver cheio, novas mensagens serão descartadas, preservando o uso eficiente de memória.
Para diferentes estratégias de descarte:
DropOldest: Remove a mensagem mais antiga para liberar espaço.DropNewest: Descarta a mensagem mais recente.Wait: Aguarda espaço livre antes de aceitar novas mensagens.
Padrões de Uso para Channels
1. Único Produtor, Único Consumidor
Este é o caso mais simples, onde um produtor gera dados e um único consumidor os processa:
var channel = Channel.CreateUnbounded<int>();
Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consumidor recebeu: {item}");
}
});
2. Múltiplos Produtores, Único Consumidor
Ideal quando há várias fontes gerando mensagens, mas apenas um consumidor processando.
var channel = Channel.CreateUnbounded<int>();
// Produtor 1
Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await channel.Writer.WriteAsync(i);
}
});
// Produtor 2
Task.Run(async () =>
{
for (int i = 5; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
}
});
// Consumidor único
Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consumidor processou: {item}");
}
});
3. Único Produtor, Múltiplos Consumidores
Este padrão distribui mensagens entre vários consumidores, útil para balanceamento de carga.
var channel = Channel.CreateUnbounded<int>();
// Produtor
Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
// Múltiplos consumidores
for (int j = 0; j < 3; j++)
{
int consumerId = j;
Task.Run(async () =>
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Consumidor {consumerId} processou: {item}");
}
});
}
Conclusão
Os Channels são uma poderosa ferramenta para implementar comunicação assíncrona em C#, facilitando a interação entre threads de forma eficiente. Eles eliminam a necessidade de bloqueios explícitos e permitem a construção de sistemas mais escaláveis e responsivos.
Principais vantagens:
- Comunicação eficiente entre produtores e consumidores.
- Suporte a limitação de buffer para evitar consumo excessivo de memória.
- Possibilidade de uso em múltiplas threads para balanceamento de carga.
Ao escolher entre BoundedChannel e UnboundedChannel, considere a taxa de produção e consumo para evitar sobrecarga de memória e garantir um fluxo de processamento adequado.