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:
- 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.
- 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.