O que significa “sincronizar” threads?

No post passado eu disse que o problema com as threads é o da sincronia. Isso significa que, se as threads compartilham recursos (variáveis globais, por exemplo) então o paralelismo pode causar problemas sérios. Observe o seguinte cenário:

#include <stdio.h>
#include <pthread.h>

int x = 1;
int terminate = 0;

void *mythread(void *data)
{
  while (!terminate)
  {
    x++;
    printf("Thread %d: x = %d\n", *(int *)data, x);

    /* Isso está aqui para não causar consumo excessivo de CPU. */
    sleep(0);
  }

  return NULL;
}

int main(int argc, char **argv)
{
  pthread_id tid1, tid2;
  int n1 = 1, n2 = 2;
  pthread_create(&tid1, NULL, mythread, (void *)&n1);
  pthread_create(&tid2, NULL, mythread, (void *)&n2);

  // alguma coisa aqui fará terminate = 1, para encerrar as threads.
  // Por exemplo:
  //
  // sleep(10);
  // terminate = 1;

  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);

  return 0;
}

O código acima tem um problema! As duas threads criadas alteram a variável global ‘x’ e, como threads são paralelizadas, você não obterá valores sequenciais em cada thread. O que falta é impedir que ambas as threads escrevam, ao mesmo tempo, na variável ‘x’. Esse processo de impedir múltiplas escritas chama-se sincronização de threads.

Existem vários métodos (mutexes, semáforos, etc). Vou me concentrar nos mutexes aqui. Eles são mais simples, aparentemente…

Mutex é uma corruptela de mutually exclusive. Isso quer dizer que se uma thread estiver “usando” um objeto deste tipo, outra não poderá usá-lo, ao mesmo tempo (por isso o “mutualmente” e a “exclusividade”). Usar mutexes é como usar uma fechadura. Os mutexes estão, inicialmente, “destravados” e, por isso, é permitido que uma thread continue seu processamento ao passar por um mutex. Mas, uma vez que coloquemos o mutex no estado “travado”, a thread que topar com a porta “fechada” vai ficar esperando até que ela seja aberta. A analogia é mais ou menos como você tentar usar o banheiro num barzinho… É recomendável que você espere o usuário anterior sair do banheiro antes de você poder usá-lo (tá bom, não é uma analogia tão boa assim, afinal, eu não conheço suas taras! hehe).

O função mythread, acima, ficaria assim:

/* Este é um jeito rápido de inicializar um mutex! */
pthread_mutex_t mymytex = PTHREAD_MUTEX_INITIALIZER;

void *mythread(void *data)
{
  while (!terminate)
  {
    pthread_mutex_lock(&mymutex);
    x++;
    pthread_mutex_unlock(&mymutex);

    printf("Thread %d: x = %d\n", *(int *)data, x);
    sleep(0);
  }

  return NULL;
}

Note que o sincronismo só é interessante se o recurso for modificado por várias threads. Multiplas leituras não têm quaisquer problemas.

Existem outras maneiras de inicializar um mutex. Assim como as threads, eles também possuem atributos (consulte a manpage pthread_mutex_init). Para usar esses atributos você deverá usar as funções pthread_mutex_init e pthread_mutex_destroy mais ou menos da mesma maneira que mostrei no post anterior. As funções pthread_mutexattr_initpthread_mutexattr_gettype e pthread_mutexattr_settype são usadas para ajustar o tipo do mutex, mas recomendo que você lide apenas com o tipo FAST, que é o default e o descrito ai em cima…

Mutexes, no linux, são equivalentes diretos das critical sections, no Windows. Quer dizer: são lightweight (diferente dos Mutexes no Windows!).

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