Cleverton Bueno
← Voltar para todos os artigos

Nos Bastidores da Performance - Uma Análise Técnica do Código-Fonte

Uma análise técnica do código-fonte da solução que otimizou a carga de XMLs, aplicando os conceitos de concorrência como produtor-consumidor, isolamento de recursos e operações atômicas para alcançar uma performance extrema.

No nosso artigo-base, o caso de estudo "De 15.000 a 400.000 por Hora", vimos o resultado impressionante de uma otimização de software. Agora, vamos abrir o capô e, com a ajuda de todo o conhecimento teórico que adquirimos na nossa série, dissecar o código-fonte para entender como essa mágica foi feita.

Veremos que não há mágica, mas sim a aplicação deliberada de sólidos princípios de engenharia de software, concorrência e otimização de I/O.

Contexto:

O código foi escrito em C++Builder XE6, utilizando a sua biblioteca VCL (Visual Component Library). Embora algumas classes sejam específicas deste ambiente (TForm, TADOConnection, TThread), os conceitos por trás delas são universais e aplicam-se a qualquer projeto C++ moderno.

1. A Arquitetura Geral e o Disparo da Ação (Button)

Toda a operação começa com o clique de um botão. Uma função é a que ira iniciar a nossa orquestra, responsável por preparar o palco, chamar os músicos (as threads) e garantir que o espetáculo termine de forma limpa.

A Preparação e a Segurança:

A função começa por garantir que as conexões com a base de dados estão ativas e, crucialmente, envolve toda a lógica principal num bloco try...catch...finally. Isto é um pilar de código robusto:

  • O try...catch garante que qualquer erro inesperado durante o processamento (uma falha de rede, um XML corrompido) seja capturado, impedindo que o programa crashe e permitindo que uma mensagem de erro seja exibida.

  • O __finally (específico do C++Builder, equivalente a um objeto RAII no C++ Padrão) garante que os recursos de memória, como FilaDeArquivos e DBLock, sejam libertados, quer a operação tenha sucesso ou falhe.

O Padrão Produtor-Consumidor em Ação:

Esta função implementa perfeitamente o padrão que discutimos no Artigo "Threads que Coordenam - condition_variable, future e Semáforos".

  • Os Consumidores:
    Primeiro, o código cria e inicia as 4 TConsumidorThread. Elas começam a correr, mas encontram a fila de trabalho vazia e ficam à espera.

  • O Produtor:
    Em seguida, a função LePastasRecursivamente é chamada. Ela atua como o Produtor, varrendo as pastas à procura de arquivos .xml e enchendo a FilaDeArquivos (a nossa fila de trabalho partilhada).

  • A Espera:
    O loop while (FilaDeArquivos->LockList()->Count > 0) é a thread principal a observar o trabalho. Ela espera pacientemente enquanto as threads consumidoras esvaziam a fila.

O Encerramento Limpo (ShutdownThreads):

Após a fila estar vazia, a função ShutdownThreads é chamada. Como vimos no Artigo "O Ciclo de Vida das Threads - Quantas, Quando e Como Parar?", ela implementa o Cancelamento Cooperativo. Ela avisa as threads para terminarem (Terminate()) e depois espera por elas (WaitFor()), garantindo uma finalização graciosa sem deixar recursos pendurados.

2. O Coração do Sistema - A Thread Consumidora (TConsumidorThread)

A verdadeira magia da performance acontece dentro da classe TConsumidorThread. Vamos analisar as suas duas partes mais importantes.

O Construtor (Isolamento de Recursos):

No construtor, vemos uma decisão de design crucial. Através de um switch(num), cada uma das 4 threads é associada a uma TADOConnection diferente (ADOConnection1 a ADOConnection4). Além disso, cada thread cria a sua própria instância de TADOQuery.
Isto é a solução prática para o problema de contenção de recursos que discutimos. Em vez de 4 threads a "lutarem" por uma única ligação à base de dados, protegida por um mutex lento, cada uma tem a sua própria "autoestrada" privada para o SQL Server. Isto permite que as escritas na base de dados ocorram em verdadeiro paralelismo, sendo um dos principais fatores para o salto de performance.

O Loop Principal (Execute):

Este método é onde a thread passa a sua vida.

  • while (!Terminated):
    Este loop é a implementação do padrão de Cancelamento Cooperativo. A thread continua a trabalhar enquanto a propriedade Terminated (controlada pela thread principal) for falsa.

  • Consumindo em Lotes:
    Dentro do loop, a thread tranca a fila partilhada (FilaDeArquivos->LockList()) e, em vez de pegar apenas um arquivo, ela tenta pegar um lote de até maxarray (50, neste caso) arquivos de uma só vez. Isto reduz o número de vezes que as threads precisam de disputar o acesso à fila, diminuindo a contenção.

  • O Pulo do Gato Atómico (InterlockedIncrement):
    Logo após pegar uma pasta, a linha InterlockedIncrement(&g_counter) é executada. Esta é a aplicação prática do que vimos no Artigo "A Velocidade da Luz - Programação Lock-Free com std::atomic" sobre operações atómicas. Em vez de usar um TCriticalSection (o equivalente a um std::mutex) para proteger um contador global, o que seria lento, o código usa uma função da API do Windows que se traduz diretamente numa instrução de hardware atómica. É a forma mais rápida possível de se obter um ID único e seguro para cada arquivo num ambiente multithreaded.

3: O Segredo da Velocidade - O Processamento em Lote

Esta função é onde a estratégia de "minimizar viagens" se materializa e onde o maior ganho de performance é alcançado.

  • A Mudança de Paradigma:
    O segredo desta função é que ela não executa uma query por arquivo. O seu único objetivo é construir Strings gigantescas que contêm os comandos INSERT para um lote inteiro de arquivos.

  • A Construção das Strings SQL:
    Dentro do loop while, para cada arquivo do lote, o código extrai os dados do XML e, em vez de chamar ExecSQL, ele simplesmente concatena os valores na String apropriada (s1 para a tabela XMLBRUTO, s2 para tbNFe_Cabecalho, etc.). A sintaxe VALUES (...), (...), (...) é usada para agrupar múltiplos registos num único comando INSERT.

  • Uma Viagem, Múltiplas Cargas:
    Apenas no final da função, fora do loop de processamento de arquivos, é que as Strings SQL massivas são finalmente enviadas para a base de dados com AQuery->ExecSQL().
    Isto está diretamente ligado à nossa discussão no Artigo 5.3 sobre tarefas I/O-Bound. O tempo de processamento não era gasto a ler o XML (CPU), mas sim na latência de cada comunicação com a base de dados. Ao agrupar centenas de INSERTs numa única chamada, o código reduz essa latência em 99%, resultando no salto de performance "brutal" de 111.000 para mais de 400.000 arquivos por hora.

Conclusão: A Teoria na Prática

A análise do fonte prova que os resultados extraordinários não vieram de "magia negra", mas da aplicação rigorosa dos princípios que estudámos:

  • Um padrão Produtor-Consumidor bem definido.
  • Isolamento de recursos (uma conexão por thread) para eliminar a contenção.
  • Uso de operações atómicas (InterlockedIncrement) para tarefas simples e de alta frequência.
  • Uma estratégia de processamento em lote para minimizar drasticamente o gargalo de I/O.

Este código é a demonstração final de que um profundo entendimento da teoria da concorrência e da arquitetura de sistemas é a ferramenta mais poderosa para transformar um software lento numa solução de altíssima performance.