Buscar

Programação Concorrente em C com Threads e Mutexes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 19 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 19 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 19 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

Philosophers - Code quoi (tradução) 1
�
Philosophers - Code quoi 
(tradução)
 Threads, Mutexes e Programação concorrente em C
 Artigo originalmente escrito por 
Mia Combeau.
 Disponível em: 
https://www.codequoi.com/en/threads-mutexes-and-concurrent-programming-in-c/
Em prol da eficiência ou por necessidade, um programa pode ser concomitante em vez 
de sequencial. Graças à sua programação concorrente através de child processes, 
threads ou mutexes, será capaz de executar múltiplas tarefas em simultâneo.
Num artigo anterior, chegámos a compreender como criar child processes, que são 
uma forma de implementar a programação concorrente. Aqui, concentrar-nos-emos nas 
threads e em como lidar com os perigos que advêm da sua memória partilhada com os 
mutexes.
Programação Concorrente
Ao contrário da programação sequencial, a programação concorrente permite que um 
programa execute várias tarefas simultaneamente em vez de ter de esperar que o 
resultado de uma operação passe para a seguinte. O próprio sistema operativo utiliza 
este conceito para satisfazer as expectativas dos seus utilizadores. Se tivéssemos de 
esperar que uma música terminasse para poder abrir o nosso navegador, ou se 
tivéssemos de reiniciar o computador para matar um programa apanhado num loop 
infinito, morreríamos de frustração!
Há três maneiras de implementar a simultaneidade nos nossos programas: processos, 
threads, e multiplexagem. Concentremo-nos nas threads.
O que é uma thread?
https://www.codequoi.com/en/threads-mutexes-and-concurrent-programming-in-c/
https://www.codequoi.com/en/threads-mutexes-and-concurrent-programming-in-c/#:~:text=how%20to%20create%20child%20processes
Philosophers - Code quoi (tradução) 2
Uma thread de execução é uma sequência lógica de instruções dentro de um processo 
que é automaticamente gerido pelo kernel do sistema operativo. Um programa 
sequencial regular tem uma única thread, mas os sistemas operativos modernos 
permitem-nos criar várias threads nos nossos programas, todas elas executadas em 
paralelo.
Cada uma das threads de um processo tem o seu próprio contexto: a sua própria 
identificação, a sua própria stack, o seu próprio ponteiro de instruções, o seu próprio 
registo de processador. Mas como todos os threads fazem parte do mesmo processo, 
partilham o mesmo espaço de endereços de memória virtual: o mesmo código, a 
mesma heap, as mesmas bibliotecas partilhadas e os mesmos files descriptors.
Uma thread tem uma pegada menor em termos de gasto computacional do que um 
processo. O que significa que é muito mais rápido para o sistema criar uma thread do 
que criar um processo. Mudar de uma thread para outra, em comparação com mudar 
de um processo para outro, é também mais rápido.
Os threads não têm a hierarquia rigorosa de pais e filhos que os processos têm. Em 
vez disso, formam um grupo de pares, independente se determinada threads criou 
tantas outras. A única distinção que o thread "principal" tem é ser o primeiro a existir no 
início do processo. Isto significa que dentro do mesmo processo, qualquer thread pode 
esperar que qualquer outro thread seja completado, ou matar qualquer outro thread.
Além disso, qualquer thread pode ler e escrever na mesma memória virtual, o que torna 
a comunicação entre os threads muito mais fácil do que a comunicação entre 
processos. Examinaremos mais tarde os problemas que podem surgir desta memória 
partilhada.
Usando threads POSIX:
A interface padrão em C para manipular as thread é o POSIX com a sua biblioteca 
<pthread.h>. Contém cerca de sessenta funções para criar e juntar threads, bem como 
para gerir a sua memória partilhada. Vamos estudar apenas uma fracção destas neste 
artigo. A fim de compilar um programa utilizando esta biblioteca, não podemos 
esquecer de ligá-lo com -pthread:
gcc -pthread main.c
Criando uma thread:
Philosophers - Code quoi (tradução) 3
Podemos criar uma nova thread a partir de qualquer outra thread do programa com a 
função pthread_create. O seu protótipo é:
int pthread_create(pthread_t *restrict thread, 
 
const pthread_attr_t *restrict attr, 
 
void *(*start_routine)(void *), 
 
void *restrict arg);
Vamos examinar cada argumento que temos de fornecer:
thread: um ponteiro para uma variável do tipo pthread_t, para armazenar a 
identificação da linha que vamos criar.
attr: um argumento que nos permite alterar os atributos padrão da nova thread. Isto 
ultrapassa o âmbito deste artigo e, em geral, é suficiente passar aqui o NULL.
start_routine: a função onde a thread irá iniciar a sua execução. Esta função terá 
como protótipo: void *function_name(void *arg);. Quando a thread atingir o fim 
desta função, será feito com as suas tarefas.
arg: um ponteiro para um argumento para passar para a função start_routine. Se 
quisermos passar vários parâmetros para esta função, teremos que passar um 
ponteiro para uma struct.
Quando a função pthread_create terminar, a variável thread que lhe demos deverá 
conter o ID da thread recentemente criada. A própria função retorna 0 se a criação foi 
bem sucedida, ou um código de erro se não o for.
Joining ou detaching threads:
A fim de bloquear a execução de uma thread até ao acabamento de outra thread, 
podemos utilizar a função pthread_join:
int pthread_join(pthread_t thread, void **retval);
Os seus parâmetros são os seguintes:
Philosophers - Code quoi (tradução) 4
thread: o ID da thread pelo qual este deve esperar. A thread especificado deve ser 
acoplável (passível de sofrer join, ou seja, não se deve separar - ver abaixo).
retval: um ponteiro para uma variável que pode conter o valor de retorno da função 
de rotina da thread (a função start_routine que fornecemos na sua criação). Aqui, 
não precisaremos deste valor: um simples NULL será suficiente.
A função pthread_join retorna 0 em caso de sucesso, ou um código de erro para o 
fracasso.
Note-se que só podemos esperar pela conclusão de uma thread específica. Não há 
maneira de esperar pela primeira thread terminada sem especificar um ID, como faz a 
função de espera para processos infantis.
Mas, em alguns casos, é possível e preferível não esperar pelo fim de uma thread em 
específico. Nesse caso, podemos detach a thread para dizer ao sistema operativo que 
ele pode recuperar imediatamente os seus recursos quando terminar a execução dessa 
thread. Para isso, utilizamos a função pthread_detach (normalmente logo após a 
criação dessa thread).
int pthread_detach(pthread_t thread);
Aqui, tudo o que temos de fornecer é a identificação da thread. A função retorna 0 se a 
operação foi um sucesso, ou não zero se houve um erro. Depois de destacar a thread, 
as outras threads não poderão matar ou esperar por este thread com pthread _join.
Um exemplo prático de threads:
Vamos escrever um pequeno e simples programa que cria duas threads e as une. A 
rotina de cada thread consiste apenas em escrever a sua própria identificação seguida 
de uma citação filosófica grosseiramente traduzida do romancista francês Victor Hugo.
#include <stdio.h> 
#include <pthread.h> 
 
# define NC "\e[0m" 
# define YELLOW "\e[1;33m" 
 
// thread_routine is the function the thread invokes right after its 
// creation. The thread ends at the end of this function. 
void *thread_routine(void *data) 
{ 
 pthread_t tid; 
Philosophers - Code quoi (tradução) 5
 
 // The pthread_self() function provides 
 // this thread's own ID. 
 tid = pthread_self(); 
 printf("%sThread [%ld]: The heaviest burden is to exist without living.%s\n", 
 YELLOW, tid, NC); 
 return (NULL); // The thread ends here. 
} 
 
int main(void) 
{ 
 pthread_t tid1; // First thread's ID 
 pthread_t tid2; // Second thread's ID 
 
 // Creating the first thread that will go 
 // execute its thread_routine function. 
 pthread_create(&tid1, NULL, thread_routine, NULL);printf("Main: Created first thread [%ld]\n", tid1); 
 // Creating the second thread that will also execute thread_routine. 
 pthread_create(&tid2, NULL, thread_routine, NULL); 
 printf("Main: Created second thread [%ld]\n", tid2); 
 // The main thread waits for the new threads to end 
 // with pthread_join. 
 pthread_join(tid1, NULL); 
 printf("Main: Joining first thread [%ld]\n", tid1); 
 pthread_join(tid2, NULL); 
 printf("Main: Joining second thread [%ld]\n", tid2); 
 return (0); 
}
Quando compilamos e executamos este teste, podemos ver que ambos as threads 
foram criados e imprimem as suas identificações correctamente. Se executarmos o 
Philosophers - Code quoi (tradução) 6
programa várias vezes seguidas, podemos notar que as threads são sempre criadas 
em ordem, mas por vezes, a thread principal escreve a sua mensagem antes do fio e 
vice-versa. Isto mostra que cada linha está de facto a ser executada em paralelo com a 
linha principal, e não sequencialmente.
Gerenciando a memória compartilhada das threads:
Uma das maiores qualidades das threads é que todos elas partilham a memória do seu 
processo. Cada thread tem a sua própria stack, mas os outros threads podem muito 
facilmente ter acesso a ela com um simples ponteiro. Além disso, a heap e quaisquer 
descritores de ficheiros abertos são totalmente partilhados entre as threads.
Esta memória partilhada e a facilidade com que um thread pode aceder à memória de 
outro thread também tem claramente a sua quota-parte de perigo: pode causar erros 
de sincronização desagradáveis.
Erros de sincronização:
Vamos voltar ao nosso exemplo anterior e modificá-lo para ver como a memória virtual 
partilhada das threads pode causar problemas. Vamos criar duas threads e dar a cada 
uma delas um ponteiro para uma variável no principal contendo um unsigned int, count. 
Cada linha irá iterar um certo número de vezes (definido na macro 
TIMES_TO_COUNT) e incrementar a contagem em cada iteração. Uma vez que 
existem dois threads, é claro que esperaremos que a contagem final seja exactamente 
duas vezes TIMES_TO_COUNT.
#include <stdio.h> 
#include <pthread.h> 
 
// Each thread will count TIMES_TO_COUNT times 
#define TIMES_TO_COUNT 21000 
 
#define NC "\e[0m" 
#define YELLOW "\e[33m" 
#define BYELLOW "\e[1;33m" 
#define RED "\e[31m" 
#define GREEN "\e[32m" 
 
void *thread_routine(void *data) 
Philosophers - Code quoi (tradução) 7
{ 
 // Each thread starts here 
 pthread_t tid; 
 unsigned int *count; // pointer to the variable created in main 
 unsigned int i; 
 
 tid = pthread_self(); 
 count = (unsigned int *)data; 
 // Print the count before this thread starts iterating: 
 printf("%sThread [%ld]: Count at thread start = %u.%s\n", 
 YELLOW, tid, *count, NC); 
 i = 0; 
 while (i < TIMES_TO_COUNT) 
 { 
 // Iterate TIMES_TO_COUNT times 
 // Increment the counter at each iteration 
 (*count)++; 
 i++; 
 } 
 // Print the final count when this thread 
 // finishes its own count 
 printf("%sThread [%ld]: Final count = %u.%s\n", 
 BYELLOW, tid, *count, NC); 
 return (NULL); // Thread ends here. 
} 
 
int main(void) 
{ 
 pthread_t tid1; 
 pthread_t tid2; 
 // Variable to keep track of the threads' counts: 
 unsigned int count; 
 
 count = 0; 
 // Since each thread counts TIMES_TO_COUNT times and that 
 // we have 2 threads, we expect the final count to be 
 // 2 * TIMES_TO_COUNT: 
 printf("Main: Expected count is %s%u%s\n", GREEN, 
 2 * TIMES_TO_COUNT, NC); 
 // Thread creation: 
 pthread_create(&tid1, NULL, thread_routine, &count); 
 printf("Main: Created first thread [%ld]\n", tid1); 
 pthread_create(&tid2, NULL, thread_routine, &count); 
 printf("Main: Created second thread [%ld]\n", tid2); 
 // Thread joining: 
 pthread_join(tid1, NULL); 
 printf("Main: Joined first thread [%ld]\n", tid1); 
 pthread_join(tid2, NULL); 
 printf("Main: Joined second thread [%ld]\n", tid2); 
 // Final count evaluation: 
 if (count != (2 * TIMES_TO_COUNT)) 
 printf("%sMain: ERROR ! Total count is %u%s\n", RED, count, NC); 
 else 
 printf("%sMain: OK. Total count is %u%s\n", GREEN, count, NC); 
Philosophers - Code quoi (tradução) 8
 return (0); 
}
Output:
É possível que, por acaso, na primeira vez que executemos o programa, o resultado 
seja correcto. Mas as coisas nem sempre são como parecem! A segunda vez que o 
executamos, o resultado é totalmente incorrecto. Se continuarmos a executar o 
programa várias vezes seguidas, chegaremos mesmo a perceber que está errado 
muito mais vezes do que certo... E nem sequer é previsivelmente errado: a contagem 
final varia muito de uma execução para a outra. Então, o que está a acontecer aqui?
O perigo de data race:
Se examinarmos os resultados de perto, podemos ver que a contagem final está 
correcta se e só se a primeira thread terminar a contagem antes da segunda começar. 
Sempre que as suas execuções se sobrepõem, o resultado é errado, e sempre menos 
do que o resultado esperado.
Philosophers - Code quoi (tradução) 9
Assim, o problema é que ambos as threads acedem frequentemente à mesma área de 
memória ao mesmo tempo. Digamos que a contagem é actualmente de 10. A thread 1 
lê o valor 10. Mais precisamente, copia o valor 10 para o seu registo, a fim de o 
manipular. Depois, adiciona 1 para obter um resultado de 11. Mas antes de poder 
guardar o resultado na área de memória apontada pela variável de contagem, a thread 
2 lê o valor 10. Depois, a thread 2 aumenta-o também para 11. Ambas threads 
guardam então o seu resultado e aí o temos! Em vez de incrementar a contagem uma 
vez para cada linha, acabaram por incrementá-la apenas por um no total... É por isso 
que estamos a perder contagens e o nosso resultado final está tão errado.
A esta situação chama-se uma corrida de dados. Acontece quando um programa está 
sujeito à progressão ou ao timing de outros eventos incontroláveis. É impossível prever 
se o sistema operativo escolherá a sequência correcta para as nossas threads.
De facto, se compilarmos o programa com as opções -fsanitizer=thread e -g e depois o 
executarmos, desta forma:
gcc -fsanitize=thread -g threads.c && ./a.out
Receberemos um alerta: "WARNING: ThreadSanitizer: data race".
Mas será que existe então uma forma de impedir uma thread de ler um valor enquanto 
outra o modifica? Sim, graças aos mutexes!
O que é um mutex?:
Um mutex (abreviatura em inglês de "exclusão mútua") é uma sincronização primitiva. 
É essencialmente um bloqueio que nos permite regular o acesso aos dados e impedir a 
utilização de recursos partilhados ao mesmo tempo.
Podemos pensar num mutex como a fechadura de uma porta de banheiro. Uma thread 
tranca-a para indicar que o banheiro está ocupada. As outras threads terão apenas de 
ficar pacientemente em fila até a porta ser destrancada antes de poderem tomar a sua 
vez no banheiro.
Declarando um mutex:
Philosophers - Code quoi (tradução) 10
Graças ao header <pthread.h>, podemos declarar uma variável do tipo mutex como 
esta:
pthread_mutex_t mutex;
Antes de o podermos utilizar, precisamos primeiro de o inicializar com a função 
pthread_mutex_init que tem o seguinte protótipo:
int pthread_mutex_init(pthread_mutex_t *mutex, 
 const pthread_mutexattr_t *mutexattr);
Há dois parâmetros a fornecer:
mutex: o ponteiro para uma variável do tipo pthread_mutex_t, o mutex que 
queremos inicializar.
mutexattr: um ponteiro para atributos específicos para o mutex. Não nos 
preocuparemos com este parâmetro aqui, podemos apenas dizer NULL.
A função pthread_mutex_init retorna sempre 0.
Lock e unlock no seu mutex:
Depois, para executar o lock e unlock no nosso mutex, precisamos de duas outras 
funções. Os seus protótipos são os seguintes:
int pthread_mutex_lock(pthread_mutex_t *mutex)); 
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Se o mutex sofre um unlock, o pthread_mutex_lock bloqueia-o e a thread responsável 
pela chamada torna-se o seu proprietário. Neste caso, a função termina imediatamente. 
No entanto, se o mutex já estiver bloqueadopor outra thread, pthread_mutex_lock 
suspende a execução do fio chamador até que o mutex seja desbloqueado.
A função pthread_mutex_unlock desbloqueia um mutex. Quando o unlock é chamado, 
considera-se que mutex a sofrer o unlock já sofreu lock. Portanto, a função apenas 
desbloqueia, sem verificar se a thread em questão sofreu de fato ou não um lock. 
Philosophers - Code quoi (tradução) 11
Então, é preciso ter cuidado ao arranjar pthread_mutex_lock e pthread_mutex_unlock 
no nosso código, caso contrário, poderemos receber erros de "lock order violation".
Ambas as funções retornam 0 para o sucesso ou um código de erro, caso contrário.
Destruindo um mutex:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Esta função destrói um mutex que sofreu unlock, liberando (freeing) quaisquer recursos 
que este possa conter. Na implementação do LinuxThreads das threads POSIX, 
nenhum recurso está associado a mutexes. Nesse caso, pthread_mutex_destroy não 
faz outra coisa que não seja verificar se o mutex não está bloqueado.
Exemplo de implementação de um mutex:
Podemos agora resolver o problema do nosso exemplo anterior de contagem final 
incorrecta através da utilização de um mutex. Para isso, precisamos criar uma pequena 
estrutura que contenha a variável count e o mutex que a irá proteger. Podemos então 
passar esta estrutura para as rotinas das nossas threads. 
#include <stdio.h> 
#include <pthread.h> 
 
// Each thread will count TIMES_TO_COUNT times 
#define TIMES_TO_COUNT 21000 
 
#define NC "\e[0m" 
#define YELLOW "\e[33m" 
#define BYELLOW "\e[1;33m" 
#define RED "\e[31m" 
#define GREEN "\e[32m" 
 
// This structure contains the count as well as the mutex 
// that will protect the access to the variable. 
typedef struct s_counter 
Philosophers - Code quoi (tradução) 12
{ 
 pthread_mutex_t count_mutex; 
 unsigned int count; 
} t_counter; 
 
void *thread_routine(void *data) 
{ 
 // Each thread starts here 
 pthread_t tid; 
 t_counter *counter; // pointer to the structure in main 
 unsigned int i; 
 
 tid = pthread_self(); 
 counter = (t_counter *)data; 
 // Print the count before this thread starts iterating. 
 // In order to read the value of count, we lock the mutex: 
 pthread_mutex_lock(&counter->count_mutex); 
 printf("%sThread [%ld]: Count at thread start = %u.%s\n", 
 YELLOW, tid, counter->count, NC); 
 pthread_mutex_unlock(&counter->count_mutex); 
 i = 0; 
 while (i < TIMES_TO_COUNT) 
 { 
 // Iterate TIMES_TO_COUNT times 
 // Increment the counter at each iteration 
 // Lock the mutex for the duration of the incrementation 
 pthread_mutex_lock(&counter->count_mutex); 
 counter->count++; 
 pthread_mutex_unlock(&counter->count_mutex); 
 i++; 
 } 
 // Print the final count when this thread finishes its 
 // own count, without forgetting to lock the mutex: 
 pthread_mutex_lock(&counter->count_mutex); 
 printf("%sThread [%ld]: Final count = %u.%s\n", 
 BYELLOW, tid, counter->count, NC); 
 pthread_mutex_unlock(&counter->count_mutex); 
 return (NULL); // Thread termine ici. 
} 
 
int main(void) 
{ 
 pthread_t tid1; 
 pthread_t tid2; 
 // Structure containing the threads' total count: 
 t_counter counter; 
 
 // There is only on thread here (main thread), so we can safely 
 // initialize count without using the mutex. 
 counter.count = 0; 
 // Initialize the mutex : 
 pthread_mutex_init(&counter.count_mutex, NULL); 
 // Since each thread counts TIMES_TO_COUNT times and that 
 // we have 2 threads, we expect the final count to be 
Philosophers - Code quoi (tradução) 13
 // 2 * TIMES_TO_COUNT: 
 printf("Main: Expected count is %s%u%s\n", GREEN, 
 2 * TIMES_TO_COUNT, NC); 
 // Thread creation: 
 pthread_create(&tid1, NULL, thread_routine, &counter); 
 printf("Main: Created first thread [%ld]\n", tid1); 
 pthread_create(&tid2, NULL, thread_routine, &counter); 
 printf("Main: Created second thread [%ld]\n", tid2); 
 // Thread joining: 
 pthread_join(tid1, NULL); 
 printf("Main: Joined first thread [%ld]\n", tid1); 
 pthread_join(tid2, NULL); 
 printf("Main: Joined second thread [%ld]\n", tid2); 
 // Final count evaluation: 
 // (Here we can read the count without worrying about 
 // the mutex because all threads have been joined and 
 // there can be no data race between threads) 
 if (counter.count != (2 * TIMES_TO_COUNT)) 
 printf("%sMain: ERROR ! Total count is %u%s\n", 
 RED, counter.count, NC); 
 else 
 printf("%sMain: OK. Total count is %u%s\n", 
 GREEN, counter.count, NC); 
 // Destroy the mutex at the end of the program: 
 pthread_mutex_destroy(&counter.count_mutex); 
 return (0); 
}
Vejamos se o resultado ainda está incorreto:
Philosophers - Code quoi (tradução) 14
Pronto! Agora o nosso resultado sempre sairá certo, mesmo que a segunda thread 
comece contando antes da primeira terminar. 
Atenção aos Deadlocks:
No entanto, os mutexes podem frequentemente provocar bloqueios. É uma situação 
em que cada thread espera por um recurso retido por outra thread. Por exemplo, a 
thread T1 adquiriu o mutex M1 e está à espera do mutex M2. Entretanto, a linha T2 
adquiriu mutex M2 e está à espera de mutex M1. Nesta situação, o programa 
permanece perpetuamente pendente e deve ser morto.
Um impasse também pode acontecer quando uma thread está esperando um mutex 
que já possui!
Vamos tentar demonstrar um impasse. Neste exemplo, teremos duas threads que 
precisam bloquear dois mutex, lock_1 e lock_2 antes de se poder incrementar um 
contador. As rotinas das duas threads serão ligeiramente diferentes: o primeiro fio 
bloqueará primeiro o lock_1, enquanto o fio 2 começará por trancar o lock_2...
#include <stdio.h> 
#include <pthread.h> 
 
#define NC "\e[0m" 
#define YELLOW "\e[33m" 
#define BYELLOW "\e[1;33m" 
#define RED "\e[31m" 
#define GREEN "\e[32m" 
 
typedef struct s_locks 
{ 
 pthread_mutex_t lock_1; 
 pthread_mutex_t lock_2; 
 unsigned int count; 
} t_locks; 
 
// The first thread invokes this routine: 
void *thread_1_routine(void *data) 
{ 
 pthread_t tid; 
 t_locks *locks; 
 
Philosophers - Code quoi (tradução) 15
 tid = pthread_self(); 
 locks = (t_locks *)data; 
 printf("%sThread [%ld]: wants lock 1%s\n", YELLOW, tid, NC); 
 pthread_mutex_lock(&locks->lock_1); 
 printf("%sThread [%ld]: owns lock 1%s\n", BYELLOW, tid, NC); 
 printf("%sThread [%ld]: wants lock 2%s\n", YELLOW, tid, NC); 
 pthread_mutex_lock(&locks->lock_2); 
 printf("%sThread [%ld]: owns lock 2%s\n", BYELLOW, tid, NC); 
 locks->count += 1; 
 printf("%sThread [%ld]: unlocking lock 2%s\n", BYELLOW, tid, NC); 
 pthread_mutex_unlock(&locks->lock_2); 
 printf("%sThread [%ld]: unlocking lock 1%s\n", BYELLOW, tid, NC); 
 pthread_mutex_unlock(&locks->lock_1); 
 printf("%sThread [%ld]: finished%s\n", YELLOW, tid, NC); 
 return (NULL); // The thread ends here. 
} 
 
// The second thread invokes this routine: 
void *thread_2_routine(void *data) 
{ 
 pthread_t tid; 
 t_locks *locks; 
 
 tid = pthread_self(); 
 locks = (t_locks *)data; 
 printf("%sThread [%ld]: wants lock 2%s\n", YELLOW, tid, NC); 
 pthread_mutex_lock(&locks->lock_2); 
 printf("%sThread [%ld]: owns lock 2%s\n", BYELLOW, tid, NC); 
 printf("%sThread [%ld]: wants lock 1%s\n", YELLOW, tid, NC); 
 pthread_mutex_lock(&locks->lock_1); 
 printf("%sThread [%ld]: owns lock 1%s\n", BYELLOW, tid, NC); 
 locks->count += 1; 
 printf("%sThread [%ld]: unlocking lock 1%s\n", BYELLOW, tid, NC); 
 pthread_mutex_unlock(&locks->lock_1); 
 printf("%sThread [%ld]: unlocking lock 2%s\n", BYELLOW, tid, NC); 
 pthread_mutex_unlock(&locks->lock_2); 
 printf("%sThread [%ld]: finished.%s\n", YELLOW, tid, NC); 
 return (NULL); // The thread ends here. 
} 
 
int main(void) 
{ 
 pthread_t tid1; // ID of the first thread 
 pthread_t tid2; // ID of the second thread 
 t_locks locks; // Structure containing 2 mutexes 
 
 locks.count = 0; 
 // Initialize both mutexes : 
 pthread_mutex_init(&locks.lock_1, NULL); 
 pthread_mutex_init(&locks.lock_2, NULL); 
 // Thread creation: 
 pthread_create(&tid1, NULL, thread_1_routine, &locks); 
 printf("Main: Created firstthread [%ld]\n", tid1); 
 pthread_create(&tid2, NULL, thread_2_routine, &locks); 
Philosophers - Code quoi (tradução) 16
 printf("Main: Created second thread [%ld]\n", tid2); 
 // Thread joining: 
 pthread_join(tid1, NULL); 
 printf("Main: Joined first thread [%ld]\n", tid1); 
 pthread_join(tid2, NULL); 
 printf("Main: Joined second thread [%ld]\n", tid2); 
 // Final count evaluation: 
 if (locks.count == 2) 
 printf("%sMain: OK. Total count is %d\n", GREEN, locks.count); 
 else 
 printf("%sMain: ERROR ! Total count is %u\n", RED, locks.count); 
 // Mutex destruction: 
 pthread_mutex_destroy(&locks.lock_1); 
 pthread_mutex_destroy(&locks.lock_2); 
 return (0); 
}
Como podemos ver na saída seguinte, na maioria das vezes, não há problema com 
esta configuração porque a primeira linha tem um pequeno avanço sobre a segunda. 
Mas por vezes, ambas as threads bloqueiam os seus primeiros mutex exactamente ao 
mesmo tempo, caso em que o programa fica bloqueado porque as threads são 
apanhadas num impasse.
Philosophers - Code quoi (tradução) 17
Estudando este segundo resultado, podemos ver claramente que a primeira thread lock 
de fio lock_1 e a segunda thread trancada lock_2. A primeira thread quer fazer lock em 
lock_2 e a segunda quer lock_1, mas nenhum deles tem qualquer forma de obter esses 
mutexes. Eles estão bloqueados.
Lidando com Deadlocks:
Há várias maneiras de lidar com deadlocks. Entre outras coisas, podemos:
ignorá-los, mas apenas se conseguirmos provar que nunca irão acontecer. Por 
exemplo, quando os intervalos de tempo entre pedidos de recursos são muito 
longos e distantes.
corrigi-los quando acontecem, matando um fio ou redistribuindo recursos, por 
exemplo. 
preveni-los e corrigi-los antes de acontecerem.
evitá-los através da imposição de uma ordem rigorosa de aquisição de recursos. 
Esta é a solução para o nosso exemplo anterior: as threads devem ambas pedir 
primeiro o lock_1.
Philosophers - Code quoi (tradução) 18
evitá-los forçando uma thread a libertar um recurso antes de pedir novos, ou antes 
de renovar o seu pedido.
Não existe uma solução "melhor" que resolva todos os casos de impasse. O melhor 
método para lidar com um determinado impasse depende da situação.
Dicas para testar as threads de um programa:
O mais importante a lembrar ao testar qualquer programa que faça uso de threads, é 
testar a mesma coisa muitas vezes seguidas. Muitas vezes, os erros de sincronização 
não serão aparentes na primeira, segunda ou mesmo terceira execução. Depende da 
ordem que o sistema operativo escolher para a execução de cada thread. Ao executar 
repetidamente o mesmo teste, podemos ver uma grande variação nos resultados.
Existem algumas ferramentas que podemos utilizar para nos ajudar a detectar erros 
relacionados com a linha, como possíveis corridas de dados, bloqueios e violações da 
ordem de bloqueio:
A flag -fsanitize=thread -g que podemos adicionar na compilação. A opção -g 
mostra os ficheiros específicos e os números de linha envolvidos.
A ferramenta de detecção de erros de threads Helgrind com a qual podemos 
executar o nosso programa, como esta: valgrind --tool=helgrind ./philo <args>.
DRD, outra ferramenta de detecção de erros de linha com a qual também podemos 
executar o nosso programa, como esta: valgrind --tool=drd ./philo <args>. 
Cuidado com a utilização de ambos -fsanitize=thread e valgrind, eles não jogam bem 
juntos!
E como sempre, não podemos esquecer de verificar fugas de memória com -
fsanitize=address e valgrind!
Uma pequena dica para partilhar, uma pergunta irritante para fazer, ou uma estranha 
descoberta para discutir sobre as threads ou mutexes? Adoraria ler e responder a tudo 
isso nos comentários. Bons códigos!
Referências Bibliográficas: 
Philosophers - Code quoi (tradução) 19
Bryant, R., O’Hallaron, D., 2016, Computer Systems: a Programmer’s Perspective, 
Chapter 12: Concurrent Programming, p. 1007 – 1076
Arpaci-Dusseau R., Arpaci-Dusseau, A., 2018, Operating Systems: Three Easy 
Pieces, Part II: Concurrency [OSTEP]
Wikipedia, Concurrent computing [Wikipedia]
Wikipedia, Mutual exclusion [Wikipedia]
The Linux Programmer Manual:
pthread_create(3) [man]
pthread_join(3) [man]
pthread_detach(3) [man]
pthread_mutex_init/lock/unlock(3) [man]
Valgrind User Manual, Helgrind: a thread error detector [cs.swan.ac.uk]
Valgrind User Manual, DRD: a thread error detector [valgrind.org]
https://pages.cs.wisc.edu/~remzi/OSTEP/
https://en.wikipedia.org/wiki/Concurrent_computing
https://en.wikipedia.org/wiki/Mutual_exclusion
https://linux.die.net/man/3/pthread_create
https://linux.die.net/man/3/pthread_join
https://linux.die.net/man/3/pthread_detach
https://linux.die.net/man/3/pthread_mutex_init
https://cs.swan.ac.uk/~csoliver/ok-sat-library/internet_html/doc/doc/Valgrind/3.8.1/html/hg-manual.html
https://valgrind.org/docs/manual/drd-manual.html

Continue navegando