Executando verificação de segurança...
14

O que são e como utilizar semaforos?

Talvez você esteja pensando: "O que tem a ver semáforo com programação?", mas calma que vou explicar tudo.

Se você procurar no Google, vai encontrar a seguinte definição: "Semáforo é um sinal de trânsito que funciona como um instrumento de controle do tráfego de automóveis e pedestres nas estradas." Agora vamos trazer esse conceito para o código. Imagine que você tem um serviço hospedado na AWS, e ele é responsável por fazer o processamento de arquivos de áudio. É necessário carregar o arquivo de áudio na memória, fazer o processamento e depois remover o arquivo da memória. Caso estejamos trabalhando com poucos arquivos, não teremos problema, mas imagine que agora temos 2 mil arquivos de áudio para processar. Serão 2 mil arquivos sendo carregados na memória... Nem preciso continuar para saber onde isso vai dar, certo?

Seria muito bom ter como limitar a quantidade de arquivos que processamos, não é mesmo? E temos, com semáforos.

O que é semaforo?

O semáforo, assim como no trânsito, serve para controlar o tráfego. No nosso exemplo, ele controlaria o tráfego de arquivos, limitando a quantidade máxima de arquivos que vamos processar simultaneamente.

Agora, vamos parar de falar e ir para um exemplo.

Exemplo real

Precisamos fazer a sincronização de usuários de uma API externa com a nossa API interna. Essa API tem as seguintes características:

  • Apenas um endpoint disponível para buscar o usuário pelo ID.
  • Usuário tem ID incremental (1, 2, 3...).
  • Os usuários não são removidos; existe um campo "removed" para sabermos se ele realmente existe ou não.

Esse é um exemplo real que já enfrentei, mas modifiquei um pouco para exemplificar melhor.

Nós temos duas opções:

  1. Importar um a um: Não é tão eficiente, vai demorar bastante tempo dependendo do tempo de resposta da API e da quantidade de usuários.
  2. Importar vários de forma controlada: Mais eficiente, pois vamos fazer mais importações em menos tempo.

Vamos fazer o codigo da primeira opção:

private static async Task ImportUsers()
{
    var cts = new CancellationTokenSource();
    int id = 1;

    try
    {
        while (true)
        {
            var user = await GetUser(cts.Token, cts, id);
            Console.WriteLine($"Successfully fetched user {id}");
            id++;
        }
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Operation was canceled.");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Execution stopped at user {id}: {ex.Message}");
    }
}

private static async Task<User> GetUser(CancellationToken token, CancellationTokenSource cts, int id)
{
    Console.WriteLine($"Getting user {id}...");

    using var client = new HttpClient();
    client.Timeout = TimeSpan.FromSeconds(10);

    try
    {
        var response = await client.GetAsync($"https://jsonplaceholder.typicode.com/users/{id}", token);

        if (!response.IsSuccessStatusCode)
        {
            cts.Cancel();
            throw new HttpRequestException($"Request for user {id} failed with status code {response.StatusCode}");
        }

        var user = await response.Content.ReadFromJsonAsync<User>(cancellationToken: token);
        return user;
    }
    catch (Exception ex)
    {
        cts.Cancel();
        throw new Exception($"Error fetching user {id}: {ex.Message}");
    }
}

Nesse caso, estamos importando os usuários um a um. Se ocorrer um erro na busca, ela é encerrada e a importação chega ao fim. Como já disse anteriormente, é pouco eficiente, pois é carregado apenas um usuário de cada vez.

Agora, vamos melhorar esse código. Vamos adicionar o semáforo e importar 10 usuários ao mesmo tempo:

private static async Task ImportUsers()
    {
        var semaphore = new SemaphoreSlim(10);
        var cts = new CancellationTokenSource();
        int id = 1;

        try
        {
            while (true)
            {
                var user = await GetUser(semaphore, cts.Token, cts, id);
                Console.WriteLine($"Successfully fetched user {id}");
                id++;
            }
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Operation was canceled.");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Execution stopped at user {id}: {ex.Message}");
        }
    }


    private static async Task<User> GetUser(SemaphoreSlim semaphore, CancellationToken token, CancellationTokenSource cts, int id)
    {
        await semaphore.WaitAsync(token);
        Console.WriteLine($"Getting user {id}...");

        using var client = new HttpClient();
        client.Timeout = TimeSpan.FromSeconds(10);

        try
        {
            var response = await client.GetAsync($"https://jsonplaceholder.typicode.com/users/{id}", token);

            if (!response.IsSuccessStatusCode)
            {
                cts.Cancel();
                throw new HttpRequestException($"Request for user {id} failed with status code {response.StatusCode}");
            }

            var user = await response.Content.ReadFromJsonAsync<User>(cancellationToken: token);
            return user;
        }
        catch (Exception ex)
        {
            cts.Cancel();
            throw new Exception($"Error fetching user {id}: {ex.Message}");
        }
        finally
        {
            semaphore.Release();
        }
    }

Para criar o semaforo nós usamos o "SemaphoreSlim". Para usá-lo, é bem simples: só precisamos criar uma nova instância dele, passando um inteiro como parâmetro. Esse inteiro vai ser a quantidade de requisições que vão ser executadas simultaneamente.

Após isso, quando iniciamos uma requisição, nós avisamos ao semáforo que uma requisição foi adicionada com await semaphore.WaitAsync(token), e quando a requisição acabar, novamente nós avisamos a ele que a requisição acabou com semaphore.Release().

Conclusão

Utilizar semáforos na programação é uma técnica eficiente para controlar o tráfego de tarefas simultâneas, assim como no trânsito. No nosso exemplo da sincronização de usuários, a implementação de semáforos permitiu gerenciar melhor os recursos e aumentar a eficiência do sistema. Com essa abordagem, podemos garantir que nossas aplicações funcionem de maneira mais eficaz e responsiva, mesmo em cenários com alta demanda.

Carregando publicação patrocinada...