Padrões comuns para aplicativos multi-threaded com mau comportamento

A Visualização Simultânea ajuda os desenvolvedores a visualizarem o comportamento de um aplicativo multithread. Essa ferramenta inclui uma galeria de padrões comuns para aplicativos multithread com mau comportamento. A galeria inclui padrões visual comuns e reconhecíveis expostos por meio da ferramenta, juntamente com uma explicação do comportamento representado por cada padrão, o resultado provável desse comportamento e a abordagem mais comum para resolvê-lo.

Contenção de bloqueio e execução serializada

Lock contention resulting in serialized execution

Às vezes, um aplicativo paralelizado continua sendo executado em série mesmo que ele tenha vários threads e que o computador tenha um número suficiente de núcleos lógicos. O primeiro sintoma é baixo desempenho multithread, talvez até mesmo um pouco mais lento do que uma implementação serial. No Modo de Exibição de Threads, você não vê vários threads sendo executados em paralelo; em vez disso, você vê que apenas um thread está sendo executado a qualquer momento. Nesse ponto, se você clicar em um segmento de sincronização em um thread, será possível ver uma pilha de chamadas para o thread bloqueado (pilha de chamadas de bloqueio) e o thread que removeu a condição de bloqueio (pilha de chamadas de desbloqueio). Além disso, se a pilha de chamadas de desbloqueio ocorrer no processo que você está analisando, um conector Preparado para Thread será exibido. A partir desse ponto, é possível navegar em seu código nas pilhas de chamadas de bloqueio e desbloqueio para investigar ainda mais a causa da serialização.

Conforme mostrado na ilustração a seguir, a Visualização Simultânea também pode expor esse sintoma no Modo de Exibição de Utilização da CPU, em que, mesmo na presença de vários threads, o aplicativo consome apenas um núcleo lógico.

Para obter mais informações, veja a seção "Começar com o problema" no artigo da MSDN Magazine Desempenho do Thread – criação de perfil de simultaneidade de contenção de recursos no Visual Studio.

Lock Contention

Distribuição de carga de trabalho não uniforme

Screenshot of a workload graph for parallel threads in the Concurrency Visualizer. The threads end at different times showing a stair-step pattern.

Quando ocorre uma distribuição irregular de trabalho entre vários threads paralelos em um aplicativo, um padrão típico de serrilha é exibido à medida que cada thread conclui seu trabalho, conforme mostrado na ilustração anterior. O Visualizador de Simultaneidade geralmente mostra horários de início muito próximos para cada thread simultâneo. No entanto, esses threads normalmente terminam de maneira irregular em vez de terminar simultaneamente. Esse padrão indica uma distribuição irregular de trabalho entre um grupo de threads paralelos, o que pode levar à redução do desempenho. A melhor abordagem para esse problema é reavaliar o algoritmo pelo qual o trabalho foi dividido entre os threads paralelos.

Conforme mostrado na ilustração a seguir, a Visualização Simultânea também pode expor esse sintoma no Modo de Exibição de Utilização da CPU como um uma etapa decrescente gradual na utilização da CPU.

Screenshot of the CPU Utilization View in the Concurrency Visualizer showing a stair-step pattern at the end of the CPU Utilization graph.

Excesso de assinatura

Screenshot of a workload graph for all active threads in the Concurrency Visualizer. A legend shows the amount of time spent in Execution and Preemption.

No caso do excesso de assinatura, o número de threads ativos em um processo é maior que o número de núcleos lógicos disponíveis no sistema. A ilustração anterior mostra os resultados do excesso de assinatura, com faixas de preempção significativa em todos os threads ativos. Além disso, a legenda mostra que uma grande percentual de tempo é gasta em preempção (84 por cento neste exemplo). Isso pode indicar que o processo está solicitando que o sistema execute mais threads simultâneos do que o número de núcleos lógicos. No entanto, isso também pode indicar que outros processos no sistema estão usando recursos que deveriam estar disponíveis para esse processo.

Você deve considerar o seguinte ao avaliar esse problema:

  • O sistema geral pode ter excesso de assinatura. Considere que outros processos no sistema talvez estejam admitindo preempção dos seus threads. Quando você pausa um segmento de preempção no modo de exibição de threads, uma dica de ferramenta identificará o thread e o processo que admitiu preempção do thread. Esse processo não é necessariamente o que foi executado durante o todo o tempo em que seu processo admitiu preempção, mas ele fornece uma dica sobre o que criou a pressão de preempção contra seu processo.

  • Avalie como o processo determina o número de threads adequado para execução durante essa fase de trabalho. Se o seu processo calcular diretamente o número de threads paralelos ativos, considere modificar esse algoritmo para representar melhor o número de núcleos lógicos disponíveis no sistema. Se você usar o Runtime de Simultaneidade, a biblioteca de paralelismo de tarefas ou PLINQ, essas bibliotecas realizam o trabalho de calcular o número de threads.

E/S ineficiente

Inefficient I/O

O uso excessivo ou incorreto de E/S é uma causa comum de ineficiências em aplicativos. Considere a ilustração anterior. O Perfil da Linha de Tempo Visível mostra que 44% do tempo de thread visível é consumido por E/S. A linha de tempo mostra grandes quantidades de E/S, que indica que o aplicativo analisado é frequentemente bloqueado por E/S. Para ver detalhes sobre os tipos de E/S e onde o seu programa está bloqueado, amplie as regiões problemáticas, examine o Perfil da Linha de Tempo Visível e, em seguida, clique em um bloco de E/S específico para ver as pilhas de chamadas atuais.

Comboios de bloqueio

Lock convoys

Comboios de bloqueio ocorrem quando o aplicativo adquire bloqueios em ordem de chegada e quando a taxa de chegada no bloqueio é maior que a taxa de aquisição. A combinação dessas duas condições faz com que as solicitações do bloqueio comecem a fazer backup. Uma maneira de combater esse problema é usar bloqueios “desleais” ou bloqueios que dão acesso ao primeiro thread para localizá-los em estados desbloqueados. A ilustração anterior mostra o comportamento desse comboio. Para resolver o problema, experimente reduzir a contenção dos objetos de sincronização e usar bloqueios desleais.

Modo de Exibição de Threads