Desafios com o uso de threads
Last updated
Last updated
O uso de threads, apesar de ser mais leve que forking de processos, traz também alguns desafios. A criação da thread pthread_create
tem um custo no sistema operacional, como podemos imaginar. O join também, e o pthread_mutex_lock
e unlock não ficam de fora.
Todas essas chamadas de funções com relação ao uso de threads causam um overhead no sistema como um todo, faz o escalonador trabalhar mais através de múltiplas trocas de contexto, e isso contribui para o aumento da latência total do sistema. Essa troca de contexto é custosa, pois envolve salvar e restaurar o estado da thread (registradores, pilha, etc) e pode causar cache misses no processador.
O sistema operacional implementa diversas otimizações para gerenciar threads de forma eficiente, mas mesmo assim, o uso indiscriminado pode levar a problemas de desempenho e complexidade.
Trabalhar com threads é complexo
Criar muitas threads em um sistema com recursos limitados pode causar degradação de desempenho devido ao aumento do overhead de gerenciamento das threads.
Por exemplo, em um sistema com 4 núcleos de CPU, criar centenas de threads geralmente não traz ganho adicional, pois a maioria delas ficará em espera devido a sincronização com mutexes. Múltiplas trocas de contexto irão piorar ainda mais o desempenho.
Para além de race conditions, podemos também enfrentar problemas muito difíceis como deadlocks, que são comuns em sistemas multithreaded e difíceis de depurar.
Em algumas situações, como por exemplo quando a T1 tem o acesso ao mutex, mas por algum motivo esse mutex ficou perdido na memória, então ela não consegue recuperar o mutex e portanto o recurso fica bloqueado para todas as outras threads, que estão à espera. Este efeito causa um deadlock, que lança um erro fatal no programa que o faz terminar imediatamente.
Por último, o sistema operacional determina uma quantidade limitada de threads. Isto por si só já nos limita bastante se precisarmos de disparar milhares de threads, como no caso de requisições HTTP em um web server. Este limite pode ser definido pelo utilitário ulimit
em sistemas UNIX-like, e é configurado tanto a nível de usuário quanto de processo.
Todas estas limitações nos levam a:
criar uma abstração de "pool" (ou piscina), onde múltiplas threads previamente criadas podem ser utilizadas e devolvidas ao pool, assim não precisamos criar milhares de threads indiscriminadamente;
recorrer a alternativas "thread-safe", que garantem de alguma forma que a memória não é compartilhada entre as threads; ou
implementar um mecanismo de "green threads", onde pequenas unidades de concorrência vivem dentro do runtime (implementação), e não necessariamente no sistema operacional. Isto requer a implementação de um escalonador para essas green threads
Vamos primeiramente abordar a implementação de uma pool de threads e como isto pode mitigar bastante os problemas inerentes ao uso de threads.