Alura > Cursos de Programação > Cursos de .NET > Conteúdos de .NET > Primeiras aulas do curso C#: Paralelismo no mundo real

C#: Paralelismo no mundo real

Usando Threads - Introdução

Olá, meu nome é Guilherme, e neste curso iremos aprender Paralelismo com C# e .NET, técnicas e recursos do processador. Aprenderemos a identificar se ele possui dois cores (que em inglês significa “núcleo”), se é um processador quad-core, com quatro cores ou mais. Iremos aprender a utilizar todos os cores para atingir o objetivo da aplicação de forma mais rápida.

Além disto, aprenderemos formas de tornar a interface gráfica mais amigável ao usuário. Vocês se lembram daquela maneira mais tradicional de desenvolvimento de aplicações? Quando temos uma ação, um processamento muito intenso, e a janela deixa de responder:

Bytebank

Na verdade, o sistema operacional até chega a achar que a aplicação travou, sendo que não é o caso, ainda há algum processamento. Este é o problema encontrado por nosso cliente, ByteBank Front Office, uma startup voltada para transações bancárias. A nossa aplicação, desenvolvida de forma tradicional, faz a consolidação da movimentação diária dos clientes e cálculos financeiros. No entanto, ela atua muito lentamente.

Além disto, a app fica com essa "cara" de estar travada, deixando o usuário sem saber o que fazer e levando-o a clicar no ícone "X" para fechar o programa. A ação pode causar um estado inválido no banco de dados, corrompendo-o, o que resulta na perda de integridade das informações do cliente.

Com técnicas de paralelismo do .NET e do C#, vamos aprender a fazer uma aplicação muito mais amigável, moderna, a partir da qual o usuário fará o processamento, vendo o progresso do que está acontecendo. Obteremos, assim, um ganho na performance, pois utilizaremos vários cores do CPU.

Se clicarmos errado em "Fazer processamento", haverá o botão de "Cancelar" logo ao lado, de forma que o banco de dados, a própria aplicação e a integridade do projeto sejam mantidos.

No curso, veremos sobre Threads, Tasks do .NET, conceitos do Task Scheduler, AsyncAwait do C#, Patterns do .NET, juntamente com todas as classes e bibliotecas.

Esses conhecimentos não valem apenas para aplicações desktop ou com interface gráfica. Podemos utilizar o Paralelismo também ao lado do servidor, para ganho de performance, fazendo-se as operações de forma mais rápida e eficiente. Vamos lá?

Usando Threads - Conhecendo o problema do cliente

Temos uma aplicação rodando, o ByteBank. Trata-se de um banco como os que já estamos acostumados, responsáveis por cuidar de conta corrente, investimentos, movimentações bancárias em geral. A nossa aplicação faz a consolidação destes dados, colhendo toda a movimentação dos clientes ao fim do dia e fazendo vários cálculos financeiros, ajustes, retornando-os posteriormente para os usuários e funcionários do ByteBank.

A app já está construída, vamos abri-la pelo programa Microsoft Visual Studio 2017 (é possível executá-la em versões anteriores também). É um projeto simples, com uma Solution e dois projetos, o ByteBank.Core e o ByteBank.View. A modelagem, repositório e serviço referentes à aplicação estão no primeiro.

Dentro do modelo, encontra-se tudo que envolve a ContaCliente, objeto que representa a conta do nosso cliente no ByteBank, como o próprio nome indica. Há NomeCliente, que armazena o nome do cliente, o decimal que mostra os investimentos, uma lista de Movimentacoes.

Pressionaremos a tecla F12 com o cursor em cima da classe Movimento, para verificarmos a implementação com mais detalhes. Nela, existem uma Data e um Tipo e, por meio deste último, o cliente pode realizar saques, depósitos, transações básicas que fazemos no dia a dia, definidas como vemos abaixo:

Transações básicas

Vamos notar que a modelagem está muito simples e poderia ser feita de várias outras formas. Porém, para nosso objetivo e tipo de aplicação, está indo muito bem.

Além do modelo, temos um repositório (ContaClienteRepository.cs), com implementação de desenvolvimento que nos retorna todos os clientes, e o serviço (ContaClienteService.cs), que faz a consolidação, aquele trabalho "bruto" comentado anteriormente, de cálculos de reajustes financeiros de todos os clientes, investimentos e afins, ao fim de cada dia. Ele possui um método público que nos retorna a string com o resultado.

Ao selecionarmos ByteBank.View no menu à direita da tela, veremos o projeto construído em cima do WPF. Vamos abrir MainWindow.xaml. As telas e janelas foram construídas nesta linguagem de marcação chamada xaml, muito semelhante ao xml. Na realidade, não precisaremos nos preocupar muito com isto, pois montar as telas em .xaml no WPF é tarefa do designer.

Precisamos saber, no entanto, que existe um botão denominado BtnProcessar. Quando clicado, ele será responsável pelo processamento. No mesmo arquivo, teremos também uma lista, chamada LstResultados.

Vamos iniciar a aplicação clicando em "Start" na barra de edição do Visual Studio. Vemos a app com o logo, um resumo em forma de texto como resultado, e o retângulo em que este resumo deve aparecer é a lista (LstResultados) citada anteriormente. Mais abaixo, veremos o botão BtnProcessar.

A reclamação do cliente é exatamente esta: a app está lenta, demorando demais para ser executada. Analisaremos o que realmente ocorre quando clicamos em "Fazer Processamento", e identificar o razão. Em seguida, fecharemos a aplicação. De volta ao Visual Studio, selecionaremos MainWindow.xaml.cs, onde se localiza o código a ser executado.

No construtor, só criamos o repositório (ContaClienteRepository();) e o serviço (ContaClienteService();). O BtnProcessar_Click é o código executado ao se clicado no botão pelo usuário. O primeiro passo consiste em obtermos todas as contas dos clientes, armazenadas na variável var contas. Montaremos uma lista vazia com o resultado usando var resultado.

Pressionaremos F12 em cima de AtualizarView para verificar sua implementação, e veremos que ele monta o resumo com a quantidade de clientes processados em determinado tempo, atualizando a lista de resultados (LstResultados), assim como a mensagem de texto. A linha abaixo é chamada para limpar a tela de qualquer resquício de processamento feito anteriormente.

AtualizarView(new list<string>(), TimeSpan.Zero);

Para obtermos as métricas, teremos um contador (var inicio = DateTime.Now;), ou seja, vamos armazenamos o início do processamento. Agora, de fato, faremos o processamento, por meio de:

foreach (var conta in contas)
{
  var resultadoConta = r_Servico.ConsolidarMovimentacao(conta);
  resultado.Add(resultadoConta);
}

Utilizando-se um laço de repetição para cada conta na lista de contas, chamaremos o serviço, faremos a consolidação de seus movimentos, adicionando-se o resultado do serviço à lista de resultado. Marcamos também o tempo de término (var fim = DateTime.Now;) e tela será atualizada com o resultado obtido.

Observe que este processamento é feito de forma totalmente independente para cada cliente. É isto que acontece na vida real, afinal, não queremos que o saldo bancário de uma pessoa seja influenciado pelo saldo de outra.

Antes de vermos exatamente o caso do cliente, vamos pensar em como as aplicações são executadas em nosso computador. Neste exemplo, teremos um computador dual core, um processador com dois núcleos de processamento. Montei um gráfico simples para mostrar o processamento de cada núcleo (core) em relação ao tempo (eixo x do gráfico).

Quando abrimos uma aplicação, neste caso o Google Chrome, o sistema operacional precisa executá-lo em um dos cores, o 1, por exemplo, que fica em atividade pelo tempo necessário para renderizar a página, carregar, fazer downloads de imagens, enquanto o outro core não possui nenhuma aplicação por enquanto.

Com o Google Chrome e o Visual Studio abertos simultaneamente, porém em núcleos distintos, o sistema operacional gerencia o processador de forma que seu uso seja otimizado ao máximo:

Gráfico que demonstra o uso de um processador dual core

Se estamos escrevendo código e ele está abrindo a janela de autocompletar, por exemplo, tudo isto é feito em outro core (2). O que acontece se abrirmos mais de uma aplicação em um mesmo núcleo? No exemplo, utilizamos o Spotify. Não temos um terceiro núcleo para colocá-lo. Neste caso, o sistema operacional ficará intermediando o tempo de uso de cada core para mais de uma aplicação.

Quando abrimos mais de uma aplicação em um mesmo núcleo

Portanto, se antes o Core 1 executava apenas o Google Chrome de forma constante, agora ele se detém, executa um pouco de um programa, pausa, começa a executar outro, alternando-se de um para o outro tão rapidamente, sem que o usuário final consiga perceber. No entanto, na prática, o tempo de execução de cada aplicação é maior, pois ele terá menos tempo de CPU para a realização deste processamento, afinal, ele estará dividindo seu core com outra aplicação.

Na situação atual temos apenas uma aplicação sendo executada, sem o Visual Studio ou o Spotify estarem funcionando em paralelo. Inclusive, o ícone do Google Chrome está bem grande nesta representação justamente porque ele possibilita que sejam abertas várias abas, das quais utilizaremos uma, enquanto as outras continuam carregando imagens e sendo renderizadas. Teremos sua performance afetada.

Temos outro core livre, sem uso. Então, o que o Chrome faz é executar várias tarefas separadamente, em cores distintos, conforme sua disponibilidade. Neste exemplo, vamos supor que temos um site aberto, com outra aba aberta também sendo processada um core diferente:

Abas distintas do Google Chrome sendo executadas em cores diferentes simultaneamente

Nós conseguimos carregar duas páginas ao mesmo tempo, com renderizações simultâneas, downloads, otimizando-se o uso do processador, afinal, trata-se de um dual core. Apenas o Google Chrome está sendo executado neste momento. Em vez de termos um grande processo que demora este tempo para ser executado, podemos dividi-lo duas linhas menores de execução. Por "linha de execução", vamos pensar do momento em que apertamos "Enter" e a página começa a ser carregada, até o fim de seu carregamento, quebrando-se, assim, o tempo necessário.

No caso do ByteBank, o que está acontecendo com seu código, e por que o cliente está reclamando? Esta é a representação do core do cliente, com apenas um processador. O ByteBank é executado no servidor dedicado, o que significa que não se trata de uma máquina em que o usuário fica utilizando o Google Chrome, Visual Studio, Spotify, ou qualquer outra aplicação do tipo ao mesmo tempo. O servidor dedicado atua apenas para esta aplicação em específico.

ByteBank

Atualmente, o que acontece é que a aplicação roda em um único core. O cliente notou a lentidão e as reclamações chegaram ao chefe, e depois ao gerente de TI, que decidiu pela aquisição de uma máquina mais potente. Passa-se a utilizar então uma máquina com dois núcleos de processamento, e espera-se que o tempo de processamento da aplicação caia pela metade, mas na verdade, ele se manteve, pois quando o ByteBank começou a ser executado, apenas um dos núcleos faz o esperado. A app só realiza uma linha de execução.

ByteBank em máquina dual core

Ao abrir a aplicação, o usuário clica em "Fazer Processamento" e, quando chega ao fim deste, tudo acontece na mesma linha de execução. Não fazemos uso de várias execuções paralelas e, por isso, mesmo usando uma máquina mais potente, o cliente não nota o ganho de performance.

O que vamos aprender a fazer agora é justamente quebrar esta aplicação em pedaços menores para otimizarmos o uso do processador da máquina do cliente.

Código fonte da aula: https://github.com/alura-cursos/Paralelismo-com-CSharp-e-.NET

Usando Threads - Criando Threads

Antes de alterarmos o código para "quebrar" a aplicação de forma a dividi-la em duas partes, voltaremos ao Visual Studio para verificar o que acontece quando a executamos em nossa máquina (a partir da tecla "Start").

Abriremos o Gerenciador de Tarefas selecionando a aba "Desempenho", que nos mostra todos os núcleos da máquina e como cada um deles está trabalhando simultaneamente. Neste exemplo, temos uma máquina com oito cores trabalhando em mais ou menos 20%, com vários programas sendo rodados ao mesmo tempo.

Desempenho pelo Gerenciador de Tarefas

Quando executamos o ByteBank e clicamos em "Fazer Processamento", vemos que do Core 2 até o último, o funcionamento não se alterou, com 20%, 30% de utilização. O único que está realmente fazendo o trabalho é o CPU 0. Se pensarmos que a máquina do cliente possui vários cores, queremos justamente utilizá-los, dividindo o trabalho entre todos eles.

No código, para cada conta, sempre executaremos uma linha, uma consolidação de cada vez. Se fossemos "quebrar" este código em algum lugar, onde seria? Inicialmente, vamos dividir contas em dois. Pensando em executar metade de um processamento destas contas em um core e a outra em outro. Mexeremos no código já definindo onde ficará cada metade. Então, vamos criar uma variável denominada contas_parte1, a qual armazenará a primeira metade da lista de contas, por meio de um método de extensão de links chamado Take(), que recebe, por parâmetro, um número inteiro que representa os n primeiros elementos a serem armazenados:

var contas_parte1 = contas.Take(contas.Count() / 2);
var contas_parte2 = contas.Skip(contas.Count() / 2);

Se vamos usar metade, precisaremos da contagem total da lista, que dividiremos por 2. Guardaremos na variável contas_parte2 a segunda metade da lista de contas, utilizando outro método de extensão, chamado Skip(), que recebe por parâmetro um número inteiro que representa os n primeiros elementos a serem pulados da lista. Neste caso, a primeira parte da lista será pulada e o restante será armazenado.

Ainda não colocamos "a mão na massa". Nota-se que usamos muito o termo "linha de execução", que na realidade é a tradução de "thread", termo técnico bastante comum na computação quando falamos sobre Paralelismo. Para criarmos uma nova linha de execução em uma aplicação, é possível fazê-lo com o objeto thread, pois é assim que ele é representado no .NET:

Thread thread_parte1 = new Thread(() =>
{
  foreach (var conta in contas_parte1)
  {
    var resultadoProcessamento = r_Servico.ConsolidarMovimentacao(conta);
    resultado.Add(resultadoProcessamento);
  }
});

O construtor default dele recebe um delegate como parâmetro, função representada pela expressão lambda, a ser compilada como função, dentro da qual haverá o código a ser executado na nossa thread. O que faremos na thread_parte1 é um processamento de todas as contas da primeira parte da nossa lista. Chamaremos a lista de contas_parte1 para cada conta na lista de contas, cujo resultado do processamento (resultadoProcessamento) será igual à chamada do serviço (r_Servico), a ser armazenando na lista de resultados (resultadoProcessamento).

O código acima será executado em uma linha de execução diferente desta, que criou a Thread:

var resultado = new List<string>();

AtualizarView(lew List<string>, TimeSpan.Zero);

var inicio = DateTime.Now;

Para continuarmos, vamos criar a thread responsável pelo processamento da parte 2, de forma totalmente independente da thread_parte1:

Thread thread_parte2 = new Thread(() =>
{
  foreach (var conta in contas_parte2)
  {
    var resultadoProcessamento = r_Servico.ConsolidarMovimentacao(conta);
    resultado.Add(resultadoProcessamento);
  }
});

O construtor foreach que existia antes destas duas threads que acabamos de criar pode ser deletado:

foreach (var conta in contas)
{
  var resultadoConta = r_Servico.ConsolidarMovimentacao(conta);
  resultado.Add(resultadoConta)
}

Executaremos a aplicação para ver se vai funcionar. Apertaremos "Start", o ByteBank é aberto, clicaremos em "Fazer Processamento". Nos aparece a mensagem "Processamento de 0 clientes em 0.0 segundos!", ou seja, não houve processamento de nenhuma conta. No Visual Studio, repassaremos o que fizemos até então: dividimos cada porção... O código parece correto.

O erro é que quando criamos a Thread, não o fazemos no momento em que começamos o processamento. Antes de terminar a função de execução do clique no botão, precisamos pedir ao sistema operacional para que ele comece a executar o código contido em thread_parte1 e thread_parte2. Faltou colocarmos isso! Só criamos estas classes, mostrando qual o código e delegate a ser executado.

Para que o trabalho delas seja iniciado, existe um método chamado Start, implementado na classe Thread, que faz exatamente isso. Portanto, acrescentaremos mais estas linhas:

thread_parte1.Start();
thread_parte2.Start();

Vamos verificar como a aplicação responde a estas alterações, repetindo aquele procedimento pelo "Start". A mensagem mostrada agora é "Processamento de 0 clientes em 0.1 segundos!". A aplicação demorou um pouco mais, porém ainda não tem nenhum processamento.

Precisamos perceber que estamos lidando com várias linhas de execução diferentes, partindo para o fim logo em seguida, sem esperar que a thread_parte1 e a thread_parte2 terminassem seus trabalhos para usarmos os resultados, mostrando-os aos usuários. A classe Thread possui uma propriedade denominada IsAlive, que retorna "verdadeiro" quando ela está em execução, e "falso" ao fim de seu processamento. Vamos utilizá-la para ficarmos presos a este método até que as threads terminem.

Sobre o curso C#: Paralelismo no mundo real

O curso C#: Paralelismo no mundo real possui 160 minutos de vídeos, em um total de 37 atividades. Gostou? Conheça nossos outros cursos de .NET em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda .NET acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri, a inteligência artificial da Alura

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas