Programação Paralela – Parte 2 – Locks em threads

O que é um lock?

Há situações em threads que precisamos fazer com que uma thread espera a outra, dado que ambas pretendem acessar um recurso que não pode ser acessado simultaneamente. Neste caso, utilizamos locks.

O problema é que sempre que uma thread espera por um lock, ela está consumindo recursos, gerando trabalho para o escalonador do sistema operacional e não está fazendo nada. Esse comportamento pode gerar problemas de desempenho.

Locks

Como explicado no post anterior (Programação paralela – Parte 1), threads compartilham o heap. Dessa maneira, eventualmente queremos evitar que as threads concorram pelos mesmos recursos. Para isso, existem objetos de sincronização, como no exemplo abaixo:

public void DoSomething()
{
    lock(this)
    {
         //Trecho que executará somente com uma thread de cada vez.
    }
}

O trecho protegido por lock, fará com que as threads não entrem simultaneamente no mesmo trecho. Existem diversos outros objetos de sincronização, inclusive que impedem que mais de um processo acesse o mesmo recurso simultaneamente. Eles ficam no namespace System.Threading.

Corrida de saco

corridadesaco

O grande problema de usarmos uma infinidade de threads com diversos pontos de sincronização (locks) é que nossas threads ficarão realizando uma espécie de corrida de sacos. Uma anda um pouquinho, mas precisa esperar a outra liberar um recurso, fazendo com que na média, tudo fique mais lento.

Para exemplificar, alterei o exemplo anterior, a parte que cada uma das threads executa, para o seguinte código:

        private void DequeueAndProcess(Queue<Job> queue)
        {
            while (true)
            {
                Job job = null;
                lock (queue)
                {
                    if (queue.Count > 0)
                        job = queue.Dequeue();

                    if (job == null)
                    {
                        Thread.Sleep(0);
                        continue;
                    }
                    job.JobDelegate();
                    job.JobEndedNotifier.Set();
                }
            }
        }

Neste caso, obtemos os seguintes tempos:

ProcessamentoParalelo-pt2-1

O exemplo é bastante exagerado, pois todo o processamento está sendo serializado dado que somente uma thread consegue processar o trecho protegido pelo lock, mas para fins didáticos, nos ajuda a entender porque muitas vezes o processamento multi-thread concorrendo por vários recursos fica pior do que o processamento single-threaded. A reposta é que além do custo do processamento, temos todo o custo de gerenciar as threads. Como desenvolvedor, não sentimos este custo, mas o sistema operacional sem dúvida sente.

Deadlocks

deadlock

Se deadlocks em bancos de dados incomodam, deadlocks em threads fazem você sentir saudades dos de banco de dados. O processo é o mesmo: duas threads que dependem de mais de um recurso tentam os obter forma inversa. O sintoma é bem diferente: as threads envolvidas no deadlock simplesmente congelam. Diferentemente do banco de dados, não existe um recurso que automaticamente escolhe uma thread como vítima, e automaticamente a mata. Você precisa desenvolver este mecanismo por sua conta.

Por sorte, deadlocks em threads são tão raros quanto um deadlock na vida real, mas como a foto acima ilustra, eles acontecem.

I/O

Como regra geral, handles também são recursos que não suportam concorrência. O mesmo mecanismo de forçar locks deve ser utilizado para evitar que duas threads tentem escrever no mesmo socket ao mesmo tempo, tentem escrever no mesmo arquivo ao mesmo tempo e assim sucessivamente. Em geral, utiliza-se mecanismos como o “lock” exemplificado acima.

Tipos thread-safe

Outra preocupação que devemos ter ao utilizar concorrência por objetos entre threads está relacionada ao tipo de dados utilizado. No MSDN há para cada tipo de dados observações sobre o tipo de dado ser thread-safe ou não (as vezes notas bastante confusas!).

Em geral, um tipo thread safe é aquele que você não precisa ter medo de acessar ou modificar em diferentes threads (Ex.: ConcurrentQueue). Tipos não thread-safe, você precisa explicitamente utilizar um lock (Ex.: Queue), o que pode gerar gargalos de desempenho, como apresentado anteriormente.

Notas importantes para aplicações Web

Aplicações web são em geral, multi-threaded sem muitas vezes nós percebermos. Geralmente cada novo request é servido numa nova thread, ou seja, se 10 usuários pedem para um servidor servir um request, é possível que tenhamos 10 threads no servidor. Utilizo as palavras geralmente e “é possível” porque o ASP.NET utiliza um mecanismo interno dele para determinar quando ou não utilizar uma nova thread, justamente para evitar contenções e baixo desempenho, conforme apresentado nesses posts. As vezes é melhor fazer um usuário esperar e os 10 que estão sendo atendidos serem bem atendidos, do que deixar 11 com péssima responsividade.

Sendo que aplicações web são essencialmente multi-thread, as mesmas preocupações devemos ter em relação a locks. Se dois requests tentam escrever num arquivo simultaneamente, um dos dois tomará uma exception. Se há alguma estrutura em memória compartilhada entre os requests (exemplos: singletons, instâncias únicas de serviço do Spring.NET, dados estáticos em classes), os mesmos problemas de concorrência estão sujeitos a acontecer.

Conclusão

Como diria um amigo: pensou que ia ser fácil, começou errado!

Processamento paralelo está longe de ser simples. É um conceito um tanto quanto complexo e na prática, debuggar aplicações multi-thread é algo bastante complexo. A própria existência do debugger, de uma linha de log, pode fazer com que as threads concorram em locais diferentes e o bug não se apresente.

Mas o recado mais importante é que não adianta pegar uma aplicação que não foi feita para ter paralelismo, que possui diversos pontos de compartilhamento de informações e sair gerando threads e entupindo de locks onde os problemas são apresentados. Muitas vezes esse processo torna a aplicação ainda menos eficiente do que era quando totalmente single-threaded.

É importante pensar numa arquitetura que suporte concorrência e escala.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s