Introdução ao Channel - Parte 1
Introdução
O recurso que abordaremos faz bom uso de concorrência e assincronismo, sendo assim existe a necessidade de esclarecer alguns pontos antes de seguir com o artigo, existe uma grande confusão por parte de muitas pessoas sobre o que é concorrência, simultaneidade e paralelismo, o problema é que concorrência é muito confundido com paralelismo, com a concorrência até conseguimos lidar com inúmeras coisas ao mesmo tempo em um único núcleo de CPU, mas isso de forma alguma quer dizer que está sendo executado de forma paralela.
Concorrência
Olhando para CPU é exatamente isso que ocorre quando temos apenas uma unidade de processamento (1 Core), convivemos com a ilusão da simultaneidade, mas o que o processador faz é apenas compartilhar um pequeno espaço de tempo entre os procedimentos para executar de forma concorrente, passando a sensação que tudo foi executado ao mesmo tempo.
Paralelismo
Quebrando teorias errôneas e falácias
O que é Channel?
O Channel surgiu exatamente para isso. 🔥🔥🔥
Channel<T> é uma classe abstrata genérica.
CreateBounded<T>(int capacity): Cria um canal delimitando a capacidade de objetos que podem ser alocados, é uma boa forma de gerenciar o que será alocado na memória.
CreateUnbounded<T>(): Cria um canal sem limitar a capacidade de objetos que podem ser alocados, ao usar método deve-se tomar muito cuidado, sabemos que recursos da máquina não são infinitos, com isso você pode sobrecarregar a memória, mas falaremos mais sobre isso na continuação deste artigo.
Cenário
- Você tem um arquivo CSV com 1000 (mil produtos)
- Precisa extrair as linhas desse arquivo
- Montar um objeto e serializar
- Enviar para um broker (SQS, Google Pub/Sub, Kafka, RabbitMQ)
Amostras de códigos
Primeiramente vamos construir nossa classe Produto, usaremos ela para representar um registro do arquivo CSV.
public class Produto
{
public string SKU { get; set; }
public string Descricao { get; set; }
public decimal Preco { get; set; }
public int Estoque { get; set; }
}
Broker Fake
Classe para simular o comportamento de envio de mensagens para um serviço de mensageria com tempo de resposta de 10 milissegundos.
public class BrokerFake
{
public static async ValueTask SendAsync<T>(T data)
{
var message = JsonSerializer.Serialize(data);
// Simular latência de 10 milissegundos
await Task.Delay(TimeSpan.FromMilliseconds(10));
}
}
Implementação de uso do Channel
public class ChannelTest<T>
{
private readonly Channel<T> _channel;
private bool _runningConsummer;
private bool _stopRequested;
public ChannelTest()
{
_channel = Channel.CreateBounded<T>(1000);
}
public async ValueTask Enqueue(T data)
=> await _channel.Writer.WriteAsync(data).ConfigureAwait(false);
public async Task Consumer()
{
while (true)
{
if(_stopRequested && _channel.Reader.Count == 0)
{
break;
}
if(_channel.Reader.Count == 0)
{
await Task.Delay(10);
continue;
}
if (_channel.Reader.TryRead(out var item))
{
await BrokerFake.SendAsync(item);
}
}
}
public void StartConsumers()
{
Task.Run(() =>
{
var tasks = new Task[6];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Consumer();
}
_runningConsummer = true;
Task.WaitAll(tasks);
_runningConsummer = false;
});
}
public void Complete()
{
_stopRequested = true;
while (_runningConsummer)
{
Task.Delay(10).Wait();
};
}
}
Teste de performance
Classe com métodos para executar testes de performance, o método GetProdutos é para abstrair o uso de um arquivo real, usaremos o BenchmarkDotNet para executar nossos testes de performance.
[MemoryDiagnoser]
public class Performance
{
private static IEnumerable<Produto> GetProdutos()
{
var produtos = Enumerable.Range(1, 1000)
.Select(p => new Produto
{
SKU = Guid.NewGuid().ToString("N"),
Descricao = $"Produto {p}",
Preco = (p * 1.1m),
Estoque = p
});
return produtos;
}
[Benchmark]
public async ValueTask SemChannel()
{
foreach (var produto in GetProdutos())
{
await BrokerFake.SendAsync(produto);
}
}
[Benchmark]
public async ValueTask ComChannel()
{
var channel = new ChannelTest<Produto>();
channel.StartConsumers();
foreach (var produto in GetProdutos())
{
await channel.Enqueue(produto);
}
channel.Complete();
}
}
Benchmark
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.22000
Intel Core i7-7500U CPU 2.70GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK=6.0.100-preview.6.21355.2
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
DefaultJob : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
| Method | Mean | Error | StdDev | Median |
|----------- |--------------:|-----------:|-----------:|--------------:|
| SemChannel | 15,945.549 ms | 31.3865 ms | 27.8233 ms | 15,941.479 ms |
| ComChannel | 1.974 ms | 0.2007 ms | 0.5726 ms | 2.209 ms |
Considerações
No próximo artigo faremos um deep-dive nas funcionalidades do Channels, abordaremos qual melhor estratégia de uso do Channel, dado que utilizamos recurso de memória e a utilização incorreta pode também degradar a performance de nossas aplicações, mas utilizando de forma correta será um grande aliado nosso, até o próximo artigo.
Contatos
twitter: @ralmsdeveloper
linkedin: @ralmsdeveloper
Deixe um comentário