Um pequeno teste com threads

Fiquei curioso… Qual é o limite prático para a quantidade de threads que fazem processamento pesado que podem ser disparadas simultaneamente? Para medir (mais ou menos) isso, eis um programinha que:

  • Não usa mecanismos de sincronização;
  • Não poe as threads para “dormir” em momento algum.

O código é esse:

#include <stdlib.h>
#include <malloc.h>
#include <pthread.h>

#define NUM_ITERATIONS 100000000

void *mythread(void *);

int main(int argc, char **argv)
{
  int numThreads;
  int i;
  pthread_t *tids;
  pthread_attr_t attr;

  numThreads = atoi(argv[1]);

  tids = (pthread_t *)malloc(sizeof(pthread_t) * numThreads);

  /* Limita o tamanho das pilhas das threads em 256 kB.
     Se não fizermos isso pthread_create() usará o
     tamanho padrão (8 MB, no Debian, 10 MB no RHEL).
     Não precisamos dessa pilha toda! */
  pthread_attr_init(&attr);
  pthread_attr_setstacksize(&attr, 262144); 

  /* NOTA: O tempo total cairá abaixo do previsto se
           pthread_create() falhar... */
  for (i = 0; i < numThreads; i++)
    if (pthread_create(tids + i, &attr, mythread, NULL) != 0)
      *(tids + i) = 0;

  /* NOTA: Ok, eu poderia ter criado threads 'detached' aqui.
           É que fiquei com preguiça de difinir o método de
           verificação de término. */
  for (i = 0; i < numThreads; i++)
    if (*(tids + i) != 0)
      pthread_join(*(tids + i), NULL);

  free(tids);   return 0; } /* Conta de 1 até NUM_ITERATIONS, sem interrupções */ void *mythread(void *data) {   int i;   for (i = 0; i < NUM_ITERATIONS; i++); }

Este código fará os “cores” de sua CPU ficarem em 100% de uso (ou quase isso). Ao executar apenas uma thread apenas um dos cores (escolhido ao acaso, pelo kernel) subirá até 100%. Duas threads, num Core 2 Duo, fará com que ambos os “cores” consumam 100%. No mesmo Core 2 Duo, 3 threads farão o mesmo, mas um dos “cores” começará a executar duas threads em time slice, e assim por diante.

Isso pode ser observado num gráfico abaixo, obtido com o seguinte script:

#!/bin/bash
for i in $(seq 1 149); do
  /usr/bin/time -a -o log.txt -f "$i %e" ./test $i;
done

Esse script gerará um arquivo com esse formato:

1 0.30
2 0.30
3 0.45
4 0.60
5 0.75
6 0.90
7 1.06
8 1.21
9 1.36
10 1.51
11 1.66
12 1.81
13 1.96
...

E o gráfico foi criado com o GNU Plot, com esse outro script:

set title "Tempo de Execução vs Threads";
set xlabel "threads";
set ylabel "tempo (em segundos)";
unset key;
plot 'log.txt' w lines;
Tempo de execução de 1 a 149 threads "gulosas".

De fato, os dados gerados mostram exatamente o comportamento que comentei anteriormente. Duas threads “contam” de 1 a 100 milhões em 0.3 segundos (lembre-se que esse teste foi feito num Core 2 Duo). Este é o mesmo tempo feito por uma única thread. O gráfico mostra uma relação linear entre o tempo e o número de threads (de duas em duas, embora não esteja tão visível asim), demonstrando o time slice.

Time slices das 1000 primeiras iterações com 8 threads.

Ok, você deve ter observado, no fragmento do log.txt que mostrei acima que existe um pequeno desbalanceamento nos tempos das threads pares e ímpares. Lembre-se que temos ainda a thread principal para ser considerada!

Outra coisa: Vocẽ pode observar que todos as CPUs (ou “cores”) estarão ocupados, mas eu não tenho certeza se as threads serão serializadas ou não. Para obter o gráfico acima tive que alterar o código, adicionando mecanismos de sincronização (mutexes) – que colocam a thread para “dormir”, se outra já o tiver “travado”. No exemplo acima a função mythread não executa funções que sejam “cancelation points” ou que, pelo menos, forcem um chaveamento de tarefa… No próximo post coloco os resultados do mesmo teste num Core 2 Quad e num i5…

Em média, poderíamos dizer que:

TempoExecução ≈ TempoUmaThread * NumThreads / NumCPUs

É claro que isso tem um limite! Vai chegar um ponto em que o tempo de execução subirá muito graças à grande carga no task switcher… Eu ainda não tive “saco” para medir, porque o tempo total de execução do loop no script em bash é o somatório de uma progressão aritmética:

Tempo total ≈ TempoUmaThread * (NumTestes + 1) * NumTestes / 2

Assim, colocar, suponha, 3000 threads em execução, gastaria cerca de quase 16 dias consecutivos de teste… De qualquer forma, é interessante observar que, mesmo que chegássemos a cerca de 200 ou 300 threads, o tempo de processamento continuaria linear. Este é o motivo que me levou a inferir (aqui) que 25 threads por processo, no Apache não é lá grandes coisas — mesmo porque, as threads, no Apache, não são tão gulosas quanto as do meu teste… já a aplicação pode ser um assunto completamente diferente!.

Eu não estou recomendando o uso de um número elevado de threads só porque temos uma relação linear no tempo de resposta. Quanto mais threads, mais concorrência. Quanto mais concorrência, menor a fatia de tempo alocada para a thread e, mais ainda, mais task switchings são feitas, consumindo uma fatia do tempo da thread… O ideal seria ter uma thread por CPU ou “core”, mas isso é impraticável (hoje, pelo menos).

Como sempre, minha recomendação é: Meça o seu código! Sempre!

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

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