Executando Tasks em paralelo em aplicações .Net com o SemaphoreSlim

소개



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.

세나리오



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, envá-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.

모범 사례



Vamos criar um trecho de codigo simples onde iremos simular o processamento de 10 mil registros de forma paralela:

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 codigo 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 Task.WaitAll Dentro do método ProcessarRegistro estamos fazendo uma requisição http que demora um segundo para ser executado.

Rodando esse trecho de codigo temos o seguinte resultado no meu computador:

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


Se não executássemos essas task 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 umambiente local, sem nenhuma concorrência para utilizar os recursarsos essedam processadas pela aplicação não é uma boa ideia, facilmente você irá provocar problemas de rede, causar lentidões nos recursos e Impactar todo oambiente.

Então como ganhar velocidade no processamento com paralelismo mas sem causar problemas e lentidões no peripherale?

Conhecendo 클래스 SemaphoreSlim



O .Net possui uma classe chamada SemaphoreSlim que limita o número de Threads que podem acessar um recurso (ou vários recursos) 동시 사용 가능.

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

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 specificivamente o número inicial de solicitações que podem ser executadas executadas simultaneamente e o número máximo de solicitas

Isso Significa que após 100 tasks estarem sendo executadas em paralelo, a próxima só será executada quando uma dessas 100 terminarem, limitando semper 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 eambiente.

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 liber

Rodando o codigo com as modificações temos o seguinte resultado no meu computador:

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


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

결론



제한 사항을 적용할 수 있습니다. A classe SemaphoreSlim pode ser uma boa solução para utilizar esses recursos de maneira controlada.

참조



SemaphoreSlim Class - Microsoft Documentation
Using SemaphoreSlim to Make Parallel HTTP Requests in .NET Core
Understanding Semaphore in .NET Core

좋은 웹페이지 즐겨찾기