Thread Pool em C

Thread Pool (ou "piscina de threads") é uma abstração que resolve os problemas associados ao uso indiscriminado de threads que vimos no tópico anterior. Em vez de criar e destruir threads para cada tarefa, uma pool de threads mantém um conjunto fixo de threads, previamente criadas, que executam tarefas conforme necessário.

Assim, quando uma tarefa é concluída, a thread é devolvida ao pool e reutilizada para outras tarefas.

Isso mitiga problemas como overhead de criação de threads, limitação de recursos no sistema operacional e latência associada à troca de contexto e sincronização de threads.

Funcionamento de uma thread pool

O funcionamento de uma pool de threads é muito simples, e passa por:

  • criar um número fixo de threads

  • cria uma fila de tarefas compartilhada entre as threads

  • cada thread na pool:

    • retira uma tarefa da fila

    • executa a tarefa

    • volta a ficar disponível para a próxima tarefa

Em temos técnicos, cada thread fica em loop verificando se há tarefa nova na fila. Basicamente, o processo principal adiciona tarefas na fila (push), e depois cada uma das threads na pool fica retirando as tarefas da fila (pop):

Entretanto, o quê acontece se 2 ou mais threads tentarem fazer pop da fila ao mesmo tempo?

Pode dar ruim, né? Race condition! E o quê fazemos pra resolver race condition?

Você acertou, tá mandando bem hein? Mutex neles!

Se a gente protege o recurso com um mutex, basicamente sincronizamos o acesso entre as threads e evitamos a condição de corrida. Contudo, o quê acontece se não houver nenhuma tarefa na fila?

Quando não há nada na fila, a operação de pop da fila retorna NULL , portanto a thread fica repetindo o loop até que uma nova tarefa seja adicionada na fila. Loops são CPU-intensive, portanto o uso da CPU ficaria bastante comprometido neste caso.

Mas a biblioteca padrão traz uma técnica primitiva para lidar com isso, que basicamente são variáveis condicionais, condvar, ou simplesmente condition.

Dada uma certa condição, a thread é colocada em estado de wait, e isto pausa a execução do loop da thread, evitando o consumo desnecessário de CPU. Este mecanismo usa por trás recursos de thread signaling, que basicamente é o envio de sinais para as threads.

Assim que uma nova tarefa é adicionada na fila, é enviado o sinal de wake para a thread que detém o mutex, portanto esta ganha prioridade no escalonador e volta a executar o loop, fazendo pop da tarefa da fila.

O uso de condvar deve ser feito em conjunto com mutex, pois a única forma de saber "qual thread acordar" é justamente a thread que detém o controle do mutex naquele exato momento.

Mutex e condvar em C

Para nossa sorte, a biblioteca padrão traz suporte ao uso de condvars, através das funções pthread_cond_wait e pthread_cond_signal

Agora, vamos abordar a implementação completa de uma thread pool em C, em pequenos passos. Primeiramente, incluímos os cabeçalhos necessários para o uso das funções do programa:

Definimos também as constantes do programa:

A fila de tarefas será representada por uma simples struct, onde uma Task contém um task_id :

Agora, definimos as variáveis de sincronização, no caso o mutex e condvar:

Okay, próximo passo é a implementação da função handle , que será executada dentro de cada thread:

Quando a função handle for executada:

  • o ID da thread é enviado nos argumentos

  • inicia-se um loop infinito. Então dentro do loop:

    • bloqueia o mutex

    • verifica se há tarefas na fila. Se houver:

      • retira a tarefa da fila

      • executa a tarefa

      • desbloqueia o mutex

      • volta ao início do loop

    • Caso não há tarefas na fila (fila vazia):

      • envia um sinal condicional de wait para a thread

Simples, não?

Próximo passo é implementar a função que adiciona a tarefa na fila, a função add_task:

Repare que, depois de adicionar, é chamada a função pthread_cond_signal que envia um sinal de wake para a thread que detém o mutex. Isto faz com que a thread "acorde" e continue e execução depois do ponto do pthread_cond_wait localizado na função handle do exemplo anterior.

Por último, nos resta implementar a função main, que deve ser bastante simples:

  • cria as 4 threads na pool

    • cada thread irá iniciar um loop infinito e ficar em estado de wait até que a primeira tarefa seja adicionada na fila (como explicado anteriormente)

  • adiciona tarefas à fila, simulando um intervalo de tempo entre cada inserção de tarefa

Atenção para o último bloco entre as linhas 20 e 22: é preciso fazer o join das threads, entretanto as threads estão em loop infinito, portanto o programa nunca irá terminar, a não ser que seja utilizado o Ctrl+C .

A implementação final

Agora sim, vamos colar tudo e mostrar para a galera no churrasco:

Simplesmente maravilhoso, não?

Last updated