Skip to content

19 - Sincronização com Mutex

Na última aula aprendemos as APIs da biblioteca pthread para criar e esperar a finalização de threads. Também aprendemos a passar argumentos e receber de volta valores usando um struct alocado dinamicamente.

Aquecimento

Vamos iniciar uma revisão trabalhando em cima do arquivo soma_global.c.

Example

Abra o arquivo e analise seu conteúdo.

Exercise

Qual o papel da função soma_parcial? Explique o papel de cada atributo de struct soma_parcial_args.

Resposta

Soma parte de um array (alguns elementos). Nos argumentos, o *vetor representa um ponteiro para todos os elementos do array. Já start e end representam o range de posições as quais queremos somar.

Exercise

Como você setaria os atributos de struct soma_partical_args para obter a soma do vetor todo? E para obter somente a soma da primeira metade do vetor?

Resposta

args->vetor = vetor;
args->start = 0;
args->end = n;

Para fins didáticos, estamos atualizando diretamente a variável soma_total dentro do for.

Example

Complete as partes faltantes e rode o programa e compile o programa soma_global.c:

gcc soma_global.c -o soma_global -pthread

Execute dois testes com entrada dos arquivos in1.txt e in2.txt.

./soma_global < in1.txt

./soma_global < in1.txt

Eles dão os resultados esperados?

Exercise

Os resultados acima estarão errados. Você consegue explicar por que?

Resposta

Na operação soma = soma + spa->vetor[i]; a variável global soma está sendo lida, somada e posteriormente atribuída a ela própria. Entretando, entre a leitura e a atribuição, outra thread pode atualizar o valor da soma, tornando os resultados inconsistentes e imprevisíveis.

Sincronização usando mutex

Vamos agora trabalhar agora para corrigir este erro! Lembrando da aula, as operações possíveis são as seguintes:

  • lock - se tiver destravado, trava e continua; se não estiver espera.
  • unlock - se tiver a trava, a destrava e permite que outras tarefas travem.

Note que não existe garantia de ordem! Ou seja, se tiverem vários processos esperando por um mutex qualquer um deles pode receber o acesso. Inclusive, uma thread pode esperar "para sempre" e nunca receber o acesso. Não é provável, mas é possível.

Warning

Você pode precisar instalar o pacote manpages-posix-dev para obter as páginas do manual usadas neste roteiro.

$ sudo apt update
$ sudo apt install manpages-posix-dev

Exercise

Identifique no seu código quais linhas compõe a região crítica e onde deveriam estar as diretivas lock e unlock. Escreva abaixo suas concluões.

Resposta

Onde a soma está sendo atualizada. Damos lock antes e unlock depois da linha soma = soma + spa->vetor[i];

Example

Coloque um comentário nas linhas identificadas acima.

Já sabemos onde iremos colocar as operações de lock e unlock do mutex. Agora falta só criá-lo.

Exercise

O mutex precisa ser criado e inicializado. Onde isto deve ser feito? Como ele pode ser recebido pela função da thread?

Resposta

Podemos fazer isto na função main, antes da criação das threads. Para que ele seja recebido pela função, podemos criar um outro argumento na nossa struct soma_parcial_args!

Exercise

Consulte o manual de pthread_mutex_init e escreva abaixo como criar e inicializar um mutex.

Resposta

Podemos fazer, por exemplo:

pthread_mutex_t mutex_soma = PTHREAD_MUTEX_INITIALIZER;

Exercise

Consulte o manual de pthread_mutex_lock e pthread_mutex_unlock e escreva abaixo como usá-las.

Resposta

pthread_mutex_lock(<mutex_address>);
// Faça sua operação!
pthread_mutex_unlock(<mutex_address>);

Example

Com base nas suas respostas acima, conserte seu programa soma_global.c e verifique que ele retorna os resultados corretos.

Exercise

Agora meça o tempo de execução e anote abaixo. Compare com o original (que não funcionava) e explique a diferença.

Resposta

Podemos conferir utilizando o comando time no terminal. Percebemos um aumento no tempo com o uso do mutex. Isto ocorre devido a necessidade de sincronizar o acesso à variável global soma.

$ time soma_global_v1 < in2.txt
$ time soma_global_v2 < in2.txt

Tip

Usar mutex é muito caro! Além de acabar com o paralelismo, as operações lock e unlock também são custosas.

Economizando mutex

Nesta parte final iremos ver como diminuir o número de chamadas ao mutex.

Exercise

É necessário atualizar a variável global soma a cada iteração do for? E é possível atualizá-la somente uma vez por thread?

Resposta

Podemos criar uma variável local que acumula a soma e só atualiza soma_global no fim. Seria uma variável por thread!

Example

Implemente a ideia acima e veja se houve melhora. Salve como soma-global2.c.

O exercício acima deverá ter desempenho bom, já que limitamos a quantidade de vezes que usamos o mutex. Vamos tentar outra ideia agora.

Exercise

Precisamos da variável global? E se cada thread retornasse sua soma parcial? Como o programa poderia ser organizado para que essa ideia funcione?

Resposta

Cada thread poderia retornar na struct de argumentos o resultado de sua soma. Na função main, após cada reposta de conclusão da thread (retorno da pthread_join), poderíamos somar os resultados.

Exercise

A ideia acima precisou de mutex? Por que?

Resposta

Não, pois na função main a execução da soma seria sequencial.

Example

Implemente a ideia acima e confira os resultados. Houve melhora no desempenho?