Executando Tasks em paralelo em aplicações .Net com o SemaphoreSlim
Tags: dotnet, csharp, braziliandevs, performance
Published: 2022-03-11 12:54:56

##Introdução##
Neste artigo vamos entender como executar tasks de forma paralela, visualizar possíveis problemas que poderemos causar em nossas aplicações e aprender a utilizar a classe SemaphoreSlim para nos ajudar a gerenciar a execução das tasks.

##Cenário##
Imagine uma aplicação onde será necessário de tempos em tempos processar uma massa de dados relativamente grande (entre 1 mil à 10 mil registros) e para cada registro processado, enviá-lo em uma requisição http. Para essa massa de dados poder ser executada em um tempo aceitável será necessário efetuar processamentos em paralelo.

##Exemplificando o cenário##
Vamos criar um trecho de código simples onde iremos simular o processamento de 10 mil registros de forma paralela:

```csharp
static void Main(string[] args)
{        
    var timer = new Stopwatch();
    Console.WriteLine($"Início da execução");

    timer.Start();
    ProcessarMassaDeDados();
    timer.Stop();

    Console.WriteLine($"Tempo: {timer.Elapsed:m\\:ss\\.fff}");
    Console.ReadKey();
}

static void ProcessarMassaDeDados()
{
    var listOfTasks = new List<Task>();
    for (int i = 0; i < 10000; i++)
    {
        listOfTasks.Add(ProcessarRegistro());
    }
    Task.WaitAll(listOfTasks.ToArray());
}

static async Task ProcessarRegistro()
{
    var _httpClient = HttpClientFactory.Create();
    await _httpClient.GetAsync("http://httpstat.us/200?sleep=1000");
}
```
Como podemos ver no código acima, primeiro criamos uma lista de tasks e adicionamos a task `ProcessarRegistro` 10 mil vezes dentro dessa lista, após isso executamos todas elas em paralelo através do comando `Task.WaitAll`, esse comando aguarda a conclusão de todas as tasks para poder seguir. Dentro do método `ProcessarRegistro` estamos fazendo uma requisição http que demora um segundo para ser executado.

Rodando esse trecho de código temos o seguinte resultado no meu computador:

```cmd
Início da execução
Tempo: 1:37.821
```

Se não executássemos essas tasks em paralelo, iria levar pelo menos 10 mil segundos (166 minutos) para executar tudo, já que cada requisição dura um segundo.

Olhando esses números o código acima parece nos atender bem, porém esse código só executou bem porque estamos testando em um ambiente local, sem nenhuma concorrência para utilizar os recursos da máquina. Executar esse mesmo código em um ambiente que já possui centenas de outras requisições sendo processadas pela aplicação não é uma boa ideia, facilmente você irá provocar problemas de rede, causar lentidões nos recursos e impactar todo o ambiente.

Então como ganhar velocidade nos processamentos com paralelismo mas sem causar problemas e lentidões no ambiente? Uma das alternativas é realizar processamentos com paralelismo porém controlando o acesso aos recursos.

##Conhecendo a classe SemaphoreSlim##
O .Net possui uma classe chamada `SemaphoreSlim` que limita o número de threads que podem acessar um recurso (ou vários recursos) simultaneamente.

Vamos modificar o nosso código e adicionar essa classe para gerenciar as execuções em paralelo:

```csharp
static void Main(string[] args)
{        
    var timer = new Stopwatch();
    Console.WriteLine($"Início da execução");

    timer.Start();
    ProcessarMassaDeDadosComSemaforo();
    timer.Stop();

    Console.WriteLine($"Tempo: {timer.Elapsed:m\\:ss\\.fff}");
    Console.ReadKey();
}

static void ProcessarMassaDeDadosComSemaforo()
{
    var semaphoreSlim = new SemaphoreSlim(100, 100);
    var listOfTasks = new List<Task>();
    for (int i = 0; i < 10000; i++)
    {
        listOfTasks.Add(ProcessarRegistro(semaphoreSlim));
    }
    Task.WaitAll(listOfTasks.ToArray());
}

static async Task ProcessarRegistro(SemaphoreSlim semaphoreSlim)
{
    await semaphoreSlim.WaitAsync();

    var _httpClient = HttpClientFactory.Create();
    await _httpClient.GetAsync("http://httpstat.us/200?sleep=1000");

    semaphoreSlim.Release();
}
```

Como podemos ver, instanciamos a classe `SemaphoreSlim` e no seu construtor passamos respectivamente o número inicial de solicitações que podem ser executadas simultaneamente e o número máximo de solicitações que podem ser executadas simultaneamente. 

Isso significa que após 100 tasks estarem sendo executadas em paralelo, a próxima só será executada quando uma dessas 100 terminarem, limitando sempre a no máximo 100 tasks em paralelo.
Obs: No código acima foi usado a quantidade de 100 execuções em paralelo somente como forma de exemplo, o ideal é você calibrar esse número de acordo com o seu cenário e ambiente. 

Dentro do método `ProcessarRegistro` usamos o método `semaphoreSlim.WaitAsync()` e `semaphoreSlim.Release()`, eles que são os responsáveis respectivamente por fazer a execução aguardar e para liberar uma posição no semáforo. 

Rodando o código com as modificações temos o seguinte resultado no meu computador:

```cmd
Início da execução
Tempo: 2:23.149
```

Podemos perceber que o processamento demora um pouco mais porém os recursos do ambiente são utilizados de maneira controlada.

##Conclusão##
Quando criamos uma aplicação devemos sempre considerar que o ambiente em execução possui recursos limitados. A classe SemaphoreSlim pode ser uma boa solução para utilizar esses recursos de maneira controlada.

##Referências##
[SemaphoreSlim Class - Microsoft Documentation](https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-6.0)
[Using SemaphoreSlim to Make Parallel HTTP Requests in .NET Core](https://mustafatulun.com/using-semaphoreslim-to-make-parallel-http-requests-in-net-core/)
[Understanding Semaphore in .NET Core](https://www.c-sharpcorner.com/article/understanding-semaphore-in-net-core/)