O livro "Código de alto desempenho na plataforma .NET. 2ª edição

imagem Este livro ensinará como maximizar o desempenho do código gerenciado, idealmente sem sacrificar nenhum dos benefícios do ambiente .NET ou, na pior das hipóteses, sacrificar um número mínimo deles. Você aprenderá métodos de programação racional, aprenderá o que evitar e, provavelmente, o mais importante, como usar as ferramentas disponíveis gratuitamente para medir o nível de desempenho sem dificuldades especiais. O material de treinamento terá um mínimo de água - apenas o mais necessário. O livro fornece exatamente o que você precisa saber, é relevante e conciso, não contém muito. A maioria dos capítulos começa com informações gerais e antecedentes, seguidas de dicas específicas, definidas como uma receita, e o final com uma seção passo a passo de medição e depuração para uma ampla variedade de cenários.

Ao longo do caminho, Ben Watson mergulhará em componentes específicos do ambiente .NET, em particular, o Common Language Runtime (CLR) subjacente a ele e verá como a memória da sua máquina é gerenciada, o código é gerado, a execução multithread é organizada e muito mais é feito. . Você será mostrado como a arquitetura .NET ao mesmo tempo limita sua ferramenta de software e fornece recursos adicionais e como a escolha dos caminhos de programação pode afetar significativamente o desempenho geral do aplicativo. Como bônus, o autor compartilhará com você histórias da experiência de criar sistemas .NET muito grandes, complexos e de alto desempenho na Microsoft nos últimos nove anos.

Trecho: escolha o tamanho apropriado do conjunto de encadeamentos


Com o tempo, o conjunto de encadeamentos é configurado de forma independente, mas no início não possui um histórico e será iniciado no estado inicial. Se o seu produto de software for excepcionalmente assíncrono e usar um processador central significativamente, ele poderá sofrer custos de inicialização iniciais proibitivamente altos, dependendo da criação e disponibilidade de ainda mais threads. O ajuste dos parâmetros de inicialização ajudará a obter um estado estável mais rapidamente, para que, a partir do momento em que o aplicativo seja iniciado, você tenha à sua disposição um certo número de encadeamentos prontos:

const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads); 

Tenha cuidado aqui. Ao usar objetos de Tarefa, seu envio será baseado no número de threads disponíveis para isso. Se houver muitos deles, os objetos Task poderão sofrer agendamento excessivo, o que levará a pelo menos uma diminuição na eficiência do processador central devido à troca de contexto mais frequente. Se a carga de trabalho não for tão alta, o conjunto de encadeamentos poderá mudar para o uso de um algoritmo que pode reduzir o número de encadeamentos, elevando-o a um número menor que o especificado.

Você também pode definir o número máximo usando o método SetMaxThreads, mas essa técnica está sujeita a riscos semelhantes.

Para descobrir o número necessário de threads, deixe esse parâmetro sozinho e analise seu aplicativo em um estado estável usando os métodos ThreadPool.GetMaxThreads e ThreadPool.GetMinThreads ou contadores de desempenho que mostram o número de threads envolvidos no processo.

Não interrompa fluxos


Interromper o trabalho de threads sem coordenação com o trabalho de outros threads é um procedimento bastante perigoso. Os fluxos devem se limpar e chamá-los de método Abort não permite que eles fechem sem consequências negativas. Quando um encadeamento é destruído, partes do aplicativo estão em um estado indefinido. Seria melhor travar o programa, mas, idealmente, é necessária uma reinicialização limpa.

Para finalizar com segurança um encadeamento, você precisa usar algum tipo de estado compartilhado, e a própria função do encadeamento deve verificar esse estado para determinar quando deve ser concluído. A segurança deve ser alcançada através da coerência.

Em geral, você sempre deve usar objetos de tarefas - uma API não é fornecida para interromper uma tarefa. Para poder finalizar consistentemente um encadeamento, você deve, como observado anteriormente, usar o token CancellationToken.

Não altere a prioridade do encadeamento


Em geral, alterar a prioridade dos threads é uma tarefa extremamente malsucedida. No Windows, o envio de encadeamentos é realizado de acordo com seu nível de prioridade. Se os threads de alta prioridade estiverem sempre prontos para serem executados, os threads de baixa prioridade serão ignorados e, muito raramente, terão a chance de executar. Ao aumentar a prioridade de um encadeamento, você diz que seu trabalho deve ter precedência sobre todos os outros trabalhos, incluindo outros processos. Isso não é seguro para um sistema estável.

É melhor diminuir a prioridade do encadeamento se estiver executando algo que possa esperar até a conclusão das tarefas de prioridade normal. Um bom motivo para diminuir a prioridade de um encadeamento pode ser descobrir um encadeamento fora de controle executando um loop infinito. É impossível interromper um encadeamento com segurança, portanto, a única maneira de retornar um determinado encadeamento e recursos de processador é reiniciar o processo. Até que seja possível fechar o fluxo e fazê-lo de forma limpa, diminuir a prioridade do fluxo fora de controle será uma maneira razoável de minimizar as conseqüências. Deve-se observar que mesmo segmentos com prioridade mais baixa ainda terão a garantia de execução ao longo do tempo: quanto mais tempo eles não tiverem iniciado, maior será sua prioridade dinâmica pelo Windows. Uma exceção é a prioridade ociosa THREAD_ - PRIORITY_IDLE, na qual o sistema operacional apenas agenda um encadeamento para execução quando literalmente não tem mais nada para iniciar.

Pode haver razões bem justificadas para aumentar a prioridade do fluxo, por exemplo, a necessidade de responder rapidamente a situações raras. Mas o uso de tais técnicas deve ser muito cauteloso. O envio de encadeamentos no Windows é realizado independentemente dos processos aos quais eles pertencem; portanto, um encadeamento de alta prioridade do seu processo será iniciado às custas não apenas dos outros encadeamentos, mas também de todos os encadeamentos de outros aplicativos em execução no sistema.

Se um conjunto de encadeamentos for usado, todas as alterações de prioridade serão descartadas toda vez que um encadeamento retornar ao conjunto. Se você continuar gerenciando encadeamentos básicos ao usar a biblioteca Paralela de Tarefas, lembre-se de que várias tarefas podem ser iniciadas no mesmo encadeamento antes de retornar ao pool.

Sincronização e bloqueio de threads


Assim que a conversa chega a vários segmentos, torna-se necessário sincronizá-los. A sincronização consiste em fornecer acesso de apenas um encadeamento a um estado compartilhado, por exemplo, a um campo de classe. Geralmente, os encadeamentos são sincronizados usando objetos de sincronização, como Monitor, Semáforo, ManualResetEvent, etc. Às vezes, eles são chamados informalmente de bloqueios, e o processo de sincronização em um encadeamento específico é chamado de bloqueio.

Uma das verdades fundamentais sobre bloqueios é esta: eles nunca aumentam o desempenho. Na melhor das hipóteses, com uma primitiva de sincronização bem implementada e sem concorrência, o bloqueio pode ser neutro. Isso leva à interrupção da execução de trabalhos úteis por outros threads e ao fato de o tempo da CPU ser desperdiçado, aumenta o tempo de alternância de contexto e causa outras consequências negativas. Você precisa tolerar isso porque a correção é muito mais importante que o desempenho simples. Se o resultado incorreto é calculado rapidamente, não importa!

Antes de começar a resolver o problema do uso do aparelho de bloqueio, consideraremos os princípios mais fundamentais.

Preciso me preocupar com o desempenho?


Justifique a necessidade de aumentar a produtividade primeiro. Isso nos leva de volta aos princípios discutidos no capítulo 1. O desempenho não é igualmente importante para todo o código do seu aplicativo. Nem todo código deve sofrer otimização de n-ésimo grau. Como regra, tudo começa com o "loop interno" - o código que é executado com mais frequência ou o mais crítico para o desempenho - e se espalha em todas as direções até que os custos excedam os benefícios recebidos. Existem muitas áreas no código que são muito menos importantes em termos de desempenho. Em tal situação, se você precisar de uma fechadura, aplique-a com calma.

E agora você deve ter cuidado. Se seu trecho de código não crítico for executado em um encadeamento de um pool de encadeamentos e você o bloquear por um longo tempo, o pool de encadeamentos poderá começar a inserir mais encadeamentos para lidar com outras solicitações. Se um ou dois threads fazem isso de tempos em tempos, tudo bem. Porém, se muitos threads fazem essas coisas, pode surgir um problema, pois, por causa disso, os recursos que precisam fazer o trabalho real são gastos inutilmente. A inadvertência ao iniciar um programa com uma carga constante significativa pode causar um impacto negativo no sistema, mesmo naquelas partes para as quais o alto desempenho não é importante, devido a alternância desnecessária de contexto ou envolvimento não razoável do conjunto de encadeamentos. Como em todos os outros casos, medidas devem ser tomadas para avaliar a situação.

Você realmente precisa de uma fechadura?


O mecanismo de bloqueio mais eficaz é aquele que não é. Se você puder eliminar completamente a necessidade de sincronização de threads, essa será a melhor maneira de obter alto desempenho. Este é um ideal que não é tão fácil de alcançar. Geralmente, isso significa que você precisa garantir que não haja estado compartilhado mutável - cada solicitação que passa pelo aplicativo pode ser processada independentemente de outra solicitação ou de alguns dados mutáveis ​​centralizados (leitura / gravação). Esse recurso será o melhor cenário para obter alto desempenho.

E ainda tenha cuidado. Com a reestruturação, é fácil exagerar e transformar o código em uma bagunça bagunçada que ninguém, inclusive você, pode descobrir. Você não deve ir longe demais, a menos que a alta produtividade seja realmente um fator crítico e não possa ser alcançado de outra maneira. Transforme o código em assíncrono e independente, mas para que fique claro.

Se vários threads acabarem de ler de uma variável (e não houver dicas de gravação de um fluxo), a sincronização não será necessária. Todos os threads podem ter acesso ilimitado. Isso se aplica automaticamente a objetos imutáveis, como seqüências de caracteres ou valores de tipos imutáveis, mas pode ser aplicado a qualquer tipo de objeto se você garantir a imutabilidade de seu valor durante a leitura por vários encadeamentos.

Se houver vários encadeamentos que gravam em alguma variável compartilhada, veja se o acesso sincronizado pode ser eliminado passando para o uso de uma variável local. Se você pode criar uma cópia temporária para o trabalho, a necessidade de sincronização desaparecerá. Isso é especialmente importante para o acesso sincronizado repetido. Ao acessar novamente a variável compartilhada, você precisa passar para acessar novamente a variável local após o acesso único à variável compartilhada, como no exemplo simples a seguir de adicionar itens a uma coleção compartilhada por vários threads.

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks); 

Este código pode ser convertido da seguinte maneira:

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks); 

Na minha máquina, a segunda versão do código é executada mais que o dobro da velocidade da primeira.
Por fim, um estado compartilhado mutável é um inimigo fundamental do desempenho. Requer sincronização para segurança dos dados, o que prejudica o desempenho. Se o seu projeto tiver pelo menos a menor oportunidade de evitar o bloqueio, você estará perto de implementar um sistema multithread ideal.

Ordem de preferência de sincronização


Ao decidir se é necessário algum tipo de sincronização, deve-se entender que nem todos eles têm as mesmas características de desempenho ou comportamento. Na maioria das situações, você só precisa usar uma trava, e geralmente essa deve ser a opção original. O uso de algo que não seja o bloqueio, para justificar complexidade adicional, requer medições intensivas. Em geral, consideramos os mecanismos de sincronização na seguinte ordem.

1. Monitor de bloqueio / classe - mantém a simplicidade, a compreensibilidade do código e fornece um bom equilíbrio de desempenho.

2. A completa falta de sincronização. Livre-se de estados mutáveis ​​compartilhados, reestruture e otimize. Isso é mais difícil, mas se for bem-sucedido, basicamente funcionará melhor do que aplicar bloqueio (exceto quando erros são cometidos ou a arquitetura é degradada).

3. Métodos simples de intertravamento de intertravamento - em alguns cenários podem ser mais adequados, mas assim que a situação se tornar mais complicada, continue usando o bloqueio de trava.

E, finalmente, se você puder realmente provar os benefícios de seu uso, use bloqueios mais intricados e complexos (lembre-se: eles raramente se mostram tão úteis quanto o esperado):

  1. bloqueios assíncronos (serão discutidos mais adiante neste capítulo);
  2. todo mundo.

Circunstâncias específicas podem ditar ou impedir o uso de algumas dessas tecnologias. Por exemplo, é improvável que a combinação de vários métodos intertravados supere uma única instrução de bloqueio.

»Mais informações sobre o livro podem ser encontradas no site do editor
» Conteúdo
» Trecho

Cupom de 25% para vendedores ambulantes - .NET

Após o pagamento da versão impressa do livro, um livro eletrônico é enviado por e-mail.

Source: https://habr.com/ru/post/pt458520/


All Articles