Solucionado (ver solução)
Solucionado
(ver solução)
5
respostas

AsyncAwait está congelando o windows forms quando faço uma simulação de long-running

Olá pessoal tudo bem?

Adorei o curso mas ainda ficam muitas perguntas que ainda não consegui compreender.

Para fixar as aulas estou fazendo uns testes simulando processamento pesado porém ainda não consegui fazer esse programa rodar sem congelar a interface.

Será que alguém pode me orientar? Estou fazendo errado? Aonde estou errando?

Entender o async e await será uma coisa muito importante no meu trabalho e queria fixar bem esse conteúdo.

Segue o código:

Form1.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private CancellationTokenSource _cts;
        private int _max = 100000000;

        public Form1()
        {
            InitializeComponent();
        }

        private async void btnExecutar_Click(object sender, EventArgs e)
        {
            DesabilitarControles();
            Inicializando();

            var progresso = new Progress<string>(mensagem => progressBar1.Value++);

            try
            {
                lblStatus.Text = $"Carregando";

                var valores = await Executar(progresso);

                HabilitarControles();

                lblStatus.Text = "Processo finalizado";
            }
            catch (OperationCanceledException)
            {
                lblStatus.Text = "Operação cancelada pelo usuário";
            }
        }

        private async Task<int[]> Executar(IProgress<string> progresso)
        {
            // Exemplo 1

            IList<int> valores = Enumerable.Range(0, _max).ToList();

            var tasks = valores.Select(x =>
            {
                return Task.Factory.StartNew(() =>
                {
                    progresso.Report(x.ToString());
                    return x;
                });
            });

            // Exemplo 2

            //  var tasks = Task.Factory.StartNew(() =>
            //  {
            //      for(var count = 0;count<_max;count++)
            //      {
            //          progresso.Report(count.ToString());
            //      }
            //  
            //      return 1;
            //  });

            return await Task.WhenAll(tasks);
        }

        private void Inicializando()
        {
            lblStatus.Text = "Inicializando";

            progressBar1.Maximum = _max;
            progressBar1.Minimum = 0;
            progressBar1.Value = 0;

            _cts = new CancellationTokenSource();
        }

        private void HabilitarControles()
        {
            lblStatus.Text = "Habilitando controles";
            btnExecutar.Enabled = true;
            btnCancelar.Enabled = false;
        }

        private void DesabilitarControles()
        {
            lblStatus.Text = "Desabilitando controles";
            btnExecutar.Enabled = false;
            btnCancelar.Enabled = true;
        }

        private void btnCancelar_Click(object sender, EventArgs e)
        {
            _cts.Cancel();

            btnCancelar.Enabled = false;
        }
    }
}

No método Executar fiz 2 exemplos e ambos deixam a tela do Windows Forms congelada.

Como Saber se está gerando uma thread quando gera esse código? Porque esse código está congelando a tela?

Obrigado desde já pessoal!

5 respostas
solução!

Bom dia, Rodrigo!

As tasks implementadas em seu exemplo fazem somente 2 tarefas: invocar os métodos ToString e Report (retornar valor é uma tarefa computacionalmente tão barata que não a inclui aqui).

O processamento é tão simples que, de fato, sua aplicação não deveria travar!

Mas, outra coisa que vale notar, é que neste exemplo o método Task.Factory.StartNew é invocado 100 milhões de vezes! O processamento realizado em Task.Factory.StartNew é consideravelmente mais caro comparado às tarefas de cada Task.

O método StartNew cria uma nova instância da classe Task, aloca memória, faz chamada ao sistema operacional, gera pressão no Garbage Collector e várias outras coisas relacionadas a Task em si - note que meus exemplos se limitaram apenas na tarefa de manipular instâncias e a memória de sua máquina!

Ou seja, o real processamento em sua aplicação acontece em invocar Task.Factory.StartNew 100 milhões de vezes! E a execução está acontecendo em sua thread principal (a thread da interface gráfica, que congela sua aplicação!).

Não é comum ser necessário a criação de tantas Tasks!

Em seu segundo exemplo, ocorre o enfileiramento de todas as chamadas ao delegate usado em seu Progress: (mensagem => progressBar1.Value++). Este código também é executado na thread principal e, além disso, o código necessário para gerenciar a fila de processamento também acontece na thread principal.

Este cruzamento de informações entre threads também é algo que fere o desempenho geral da aplicação.

Você pode simular um processamento pesado fazendo operações matriciais em grandes quantidades de dados - um exemplo de caso, que eu pessoalmente gosto, é a manipulação de imagens!

Para verificar o número de threads criadas, é necessário pausar a execução pelo Visual Studio e acessar o painel Threads (Debug -> Windows -> Threads).

Executei seu código e em minha máquina foram criadas 10 threads. Por mais que este número varie, é provável que você encontre um valor semelhante, porque um dos grandes benefícios de se usar Tasks é que elas reaproveitam Threads já criadas para sua aplicação.

Olá Guilherme,

Muito obrigado pela resposta.

Aqui no trabalho utilizamos muito BackgroundWorker e Thread na unha para essas tarefas, e fazer elas dividas como vocês fez no exemplo achei super interessante porque ganhamos desempenho, porém eu fiz alguns exemplos para poder verificar a atualização da UI e "tentar" simular uma força de trabalhar com tasks.

Minha dificuldade está em entender a estrutura ideal para poder converter do background worker e as thread para as Tasks. Eu fiz uma simulação com o número astronomico para forçar mesmo o processamento longo, porém acarretou o travamento da interface.

Queria saber aqui, nesse meu exemplo, em que parte estou errando na montagem do processamento para que a interface não congele. Teria como forçar o processamento da pilha da Thread principal? Teria como alterar a forma que a Task trabalha para poder não travar a interface gráfica? Qual é o cenário ideal para se trabalhar com tasks?

Rodrigo,

Típicos cenários em que o uso de Tasks traz benefícios são aqueles em que existe um intenso uso de CPU ou há a espera por recursos de Entrada/Saída (download de arquivo, resposta de servidor remoto, etc.).

Não é qualquer uso intensivo de CPU que se encaixa no modelo de Task - o código que você nos apresentou é um exemplo.

Seu código de exemplo peca em dois aspectos:

1.) Você cria muitas Tasks para pouquissimo trabalho. Em seu exemplo, há o processamento de 100 milhões de itens por 100 milhões de Tasks.

2.) O trabalho das Tasks está sendo, basicamente, interromper a Thread principal para notificar um progresso.

Será que vale a pena, em 100 milhões de itens, notificar o processamento de cada um deles individualmente?

Trago um exemplo equivalente para o mundo prático:

Seja o coordenador de uma editora de livros responsável por um time de 5 tradutores. Será que o coordenador vai conseguir ser produtivo se ele for interrompido a cada palavra que um de seus 5 funcionários traduzir? Talvez seja mais produtivo se ele for interrompido apenas quando cada tradutor terminar um grande volume de trabalho, seja um capítulo ou um livro completo, porque caso contrário, será mais produtivo o próprio coordenador fazer as traduções.

Se você trabalha com um cenário em que existe um código intenso ou há um grande volume de dados que podem ser processados sem a necessidade de, muito frequentemente, notificar a thread principal, temos um bom caso para se usar Tasks.

Como é o tipo de código em seus BackgroundWorkers e Threads? Muito possivelmente ele se encaixe no caso de usar Tasks sem muitas alterações.

Agora to começando a entender! O que aconteceu foi um efeito colateral pela forma de simulação que estou fazendo. Sobre o processamento de imagens, que exemplo eu posso fazer pra simular esse processamento !?

Sobre o background worker e as threads aqui na empresa, temos um sistema que faz atualizações dos produtos. Basicamente preenchemos um data grid view e para cada linha do grid fazemos a coleta e atualização do produto e retornamos a linha atualizada da informação coletada.

Cada linha eh feita uma por vez e isso demora muito, porque cada coleta eh necessário coletar, processar e exibir. Tudo é feito em ordem do grid e um por vez.

Essa atualização demora demais e já estamos entrando naquele dilema, adaptar a aplicação em thread ou melhorar o servidor ?!

Esse curso caiu muito bem pra mim. Porém preciso entender. Threads é fácil porém gerenciar quais serão executadas que é o problema.

Isso tudo é o que eu vejo aqui de experiência.

Agradeço desde já o apoio !!! Muito obrigado!

Gratidão!

Se o servidor der suporte a apenas 1 entidade por requisição, suas alternativas são limitadas, porque você corre o risco de sobrecarregar o servidor com muitas chamadas simultâneas.

Talvez seja interessante criar uma interface entre esses dois sistemas e rotineiramente atualizar uma base de dados usada por sua aplicação desktop.

Se criar a interface entre os sistemas for viável, só teremos a Task responsável por recuperar isto. Se isto não for possível, eu criaria uma Task por item e ela seria responsável por coletar e retornar a linha atualizada, enquanto o usuário pode editar e criar outras linhas - com esse modelo, tome cuidado para não sobrecarregar o servidor, você pode enfileirar estas Tasks e executá-las de forma ordenada e não simultânea.