Vous êtes sur la page 1sur 34

Programacao Multithread com a biblioteca POSIX Threads

Pedro Arthur Duarte http://pedroarthur.com.br/ http://under-linux.org/blogs/pedroarthurjedi pedroarthur jedi [AT] gmail com
1

1. Introducao
Nos ultimos anos temos visto uma enxurrada de novos processadores clamando o uso de m ltiplos n cleos. Eles se aproveitam dos avancos das tecnologias de miniaturizacao u u de componentes e criam solucoes que s eram disponveis anteriormente com o uso de o diversos processadores e da duplicacao de diversos outros componentes. Esses processadores tiram proveito da paralelizacao da la de execucao. Ou seja, ao inv s de estarem processando uma unica operacao por vez, tais processadores s o e a capaz de dividir as tarefas, cada qual se dedicando a uma operacao por vez. Para tirar proveito dos recursos, as aplicacoes precisam passar por uma pequena modicacao: divis o dos uxos de execucao. Ou, como mais conhecido, as aplicacoes a precisam se tornar multithreads. Ou seja, devemos estudar e analisar as aplicacoes visando a identicacao de pon tos passveis de paralelizacao. Um exemplo simples disso seria um browser de Internet. As diferentes abas exibem p ginas distintas, portanto, o carregamento de cada aba pode a ocorrer de forma independente uma da outra. Apesar de existirem diversos tipos de paralelismo, focaremos aqui no modelo de multiprocessadores com mem ria compartilhada. o Aqui n o ser o apresentados os fundamentos do processamento multithread pois a a deixaria o texto enfadonho, principalmente para a maioria do pessoal que n o possui a interesse nem disposicao de estar analisando funcoes matem ticas. Mas, para aqueles que a desejam uma abordagem mais voltada a area da pesquisa acad mica, recomendo o livro e Introduction to Parallel Computingescrito por Grama, Karypis, Kumar e Gupta. No decorrer do texto veremos como criar aplicacoes multithreads e como contor nar os problemas envolvidos. Os exemplos estar o em C mas no futuro mostrarei como a criar aplicacoes multithread em Python e Java. Todos os exemplos assumem que voc e esteja desenvolvendo em um derivado do Linux.

2. A Biblioteca POSIX Threads


A biblioteca pthreads (POSIX Threads) e baseada nas especicacoes POSIX.1 que dene um conjunto de tipos, funcoes e macros relativos a criacao e ao controle de diversos uxos de execucao, chamados daqui para frete de threads. No modelo de processamento multithreads POSIX, as diversas threads residem no mesmo espaco de enderecamento do processo que as criou, ou seja, compartilham das mesmas vari veis, descritores de arquivo e ainda compartilham o espaco disponvel para a alocacao da pilha do processo. Portanto, mesmo que seu processo tenham 600 threads, a alocacao de mem ria estar disponvel como se fosse apenas um processo. o a

Para iniciar o uso de funcoes da biblioteca PThreads, o arquivo fonte deve incluir o cabecalho pthreads: #include <pthread.h> Al m disso, todos os comandos de compilacao devem indicar o uso da biblioteca: e $ gcc -o saida -lpthread entrada.c Doutra forma, pode esperar um monte de mensagens nada amig veis do GCC. a 2.1. Criando e terminando novas threads O padr o PThreads exige que funcoes que ser o chamadas para a criacao de novas threads a a possuam uma assinatura especca. A saber: void* (void *) Ou seja, a funcao a ser executada precisa obrigatoriamente retornar um ponteiro gen rico e receber como par metro de entrada um ponteiro gen rico. O valor de retorno, e a e como veremos adiante, pode ser recuperado e usado posteriormente. O par metro de a entrada pode ser usado para passar um dado qualquer para a nova thread. Criada a funcao com a assinatura supracitada, podemos ent o usar a funcao int a pthread create(pthread t*, pthread attr*, void*(*)(void *), void *). O primeiro par metro a receber o identicador unico da thread; o segundo par metro serve para criar uma thread a a com atributos especiais, os quais veremos mais abaixo; o terceiro par metro e a funcao a a ser executada pela thread; e o quarto par metro e o par metro de entrada da funcao. O vaa a lor de retorno de pthread create e zero caso a nova thread seja criada com sucesso, ou um valor diferente de zero caso haja algum erro. Recomendo executar man pthread createe dar uma breve lida. Analisemos o seguinte exemplo: #include <stdio.h> #include <pthread.h> void* print(void *data){ char *str = (char *) data; while (1) printf("%s ", str); } int main (int argc, char** argv) { int i; pthread_t threads[argc]; for (i = 1 ; i < argc ; i++) pthread_create(&threads[i], NULL, print, (void*) argv[i]); return 0; }

Nesse pequeno programa queremos que ela imprima na tela os par metros passaa dos ao programa, por m, ao inv s de fazer o trabalho em s rie, ser criada uma thread e e e a para cada par metro. Para tal, criamos uma pequena funcao chamada print que atende a todos os requisitos do padr o (valor de retorno e par metro do tipo void*). a a Como se pode inferir, pthread t e um tipo de dado denido pelo padr o. Sua a principal funcao e armazenar o identicado unico de cada thread. Esse valor e usado em algumas funcoes que veremos mais a frente. Como n o temos nenhuma necessidade adicional nessas threads, o segundo a par metro foi denido como nulo e ser ignorado pela funcao pthread create, que criar a a a as threads com os atributos padr es. o Assim que a thread estiver pronto para executar, o sistema dar lugar a funcao a print. Essa far um cast do quarto par metro para um char*. Esse dever ser impresso a a a tantas vezes quanto possvel. Agora, devemos compilar nosso programa. Assumindo que esse esteja salvo sob o nome threads.c: $ gcc -o threads -lpthread -Wall threads.c Agora, executemos nossa pequena aplicacao com os par metros under linux org: a pedroarthur@coruscant:/ccc$ ./a.out under linux org pedroarthur@coruscant:/ccc$ Putz! N o saiu nada! Mais uma vez: a pedroarthur@coruscant:/ccc$ ./a.out under linux org org org org org org org org org org org org org org org org org org org org org org org org org org org org org under under under under under under under u nder under under under under under under under unde r under under org under org under org linux linux l inux linux linux linux linux linux linux linux linu x under org org org org linux org linux linux linux org linux linux linux org linux org linux org linux linux linux org linux linux linux org linux org lin ux org linux org org org linux org linux org linux linux org linux linux linux org linux org linux org pedroarthur@coruscant:/ccc$ Viram que comportamento estranho? Pois e, o uso de threads pode causar efeitos colaterais indeterminados a sua aplicacao. As threads n o s o organizadas de maneira a a serial, portanto, n o se deve esperar nada de seu comportamento. a Nesse primero exemplo, a maioria devia estar esperando que underfosse escrito antes de linux, e por sua vez esse fosse escrito antes de org. Por m, na primeira e execucao, n o tivemos nada na sada. Ou seja, foi dado ao thread principal um maior a tempo de execucao. Ent o, esse avancou logo para a funcao de retorno e saiu do programa. a J na segunda execucao, as threads criadas receberam mais atencao que a thread principal a

e puderam cada qual escrever algumas vezes na tela. Ent o, tentando frisar, n o se deve a a esperar nada do comportamento das threads! Umas podem avancar mais que outras, ou talvez nem cheguem a executar! Estejam cientes! Como nota-se pelo evento, as threads s o nalizadas t o logo a funcao main rea a torna. Ent o, acredito que tenha cado a d vida de como deixar uma thread executando a u mesmo que se deseje nalizar a funcao main. Um m todo simples e nalizar a thread em e que a funcao main est executando. Em outras palavras, terminar o uxo de execucao a 1 sem utilizar a diretiva return. Ent o, bastaria fazer : a /* inicio do cdigo /* o for (i = 1 ; i < argc ; i++) pthread_create(&threads[i], NULL, print, (void*) argv[i]); pthread_exit(NULL); /* Restante do cdigo */ o A funcao pthread exit naliza a thread na qual ela e chamada. O par metro de a pthread exit deve ser substituido pelo valor que se deseja retornar. O valor de retorno pode ser recuperado por outra funcao da biblioteca PThread, que veremos um pouco mais abaixo. Por m, fazendo a thread principal terminar sua execucao dessa forma nota-se, e mediante consulta na tabela de processos, que ser concatenado o p s-xo <defunct>: a o 1000 6780 14.0 0.0 0:00 [a.out] <defunct> 0 0 pts/3 Zl+ 07:28

Ou seja, para o sistema seu processo e um zumbi. Apesar de n o ser um grande a problema, nesse caso, ter um processo nesse estado n o e considerado uma boa pr tica. a a Portanto, ao inv s de nalizar a thread principal, e mais interessante aguardar que as e outras threads nalizem usando a funcao pthread join(pthread t, void**). O primeiro par metro e o identicador unico da thread que se deseja aguardar o m da execucao. a O segundo par metro e um ponteiro gen rico que dever receber o valor de retorno da a e a thread que est sendo aguardada. a Como em nosso exemplo anterior o uxo de execucao das threads n o tem m, a vamos atualizar nosso c digo para o seguinte: o #include <stdio.h> #include <pthread.h> #include <stdlib.h> void* print(void *data){ char *str = (char *) data; int i;
1

Para nalizar a execucao pressione ctrl+c.

srandom(time(NULL)); for (i = 0 ; i < random() ; i++) printf("%s ", str); pthread_exit((void*)i); } int main (int argc, char** argv) { int i; pthread_t threads[argc]; void *retval[argc]; for (i = 1 ; i < argc ; i++) pthread_create(&threads[i], NULL, print, (void*) argv[i]); for (i = 1 ; i < argc ; i++) pthread_join (threads[i], &retval[i]); printf ("\n"); for (i = 1 ; i < argc ; i++) printf ("%s: %d\n", argv[i], (int)retval[i]); return 0; } Novamente, devemos compilar o c digo incluindo a biblioteca pthreads, assuo mindo o nome do arquivo fonte como threads.c: pedroarthur@slackhlbr:/ccc$ gcc -lpthread -Wall threads.c Rodando nossa aplicacao com os par metro the jedi lairs: a /* Um monte de repetices das palavras "the", "jedi" e "lairs" */ o the: 84570 jedi: 54323 lairs: 39598 pedroarthur@slackhlbr:/ccc$ A funcao print foi modicada para imprimir um n mero rand mico de vezes a u o string passada como par metro (linhas 11 e 12). Colocamos tamb m, no nal do c digo a e o o de print, a funcao pthread exit para retornar o valor de i ap s o m da execucao do laco (linha 14). Como o valor de i e um inteiro, e a funcao espera receber um ponteiro gen rico, tivemos que fazer um cast na vari vel i. Tenham em mente que essa solucao e a n o e port vel!!! a a J na funcao main, adicionamos um vetor de ponteiros gen ricos para receber os a e valores de retorna das threads (linha 21). Al m disso, tivemos que adicionar chamadas e

a funcao pthread join para que as mesmas recebessem os c digos de retorno (linhas 26 o e 27). Por m, adicionamos um pequeno trecho de c digo para imprimir quantas vezes o cada um dos par metros da linha de comando foram impressos (linhas 31 e 32). a Uma thread que chame a funcao pthread join car bloqueada at que a thread a e que ela est aguardando nalize. Em outras palavras, se temos as threads A e B, e A a chama pthread join(B, NULL), ela car sem executar nenhuma outra instrucao at que a e B chame pthread exit(NULL). 2.2. Atributos das Threads Os atributos das threads permitem denir determinados comportamentos para essas. Os atributos de uma thread podem ser determinados no momento da criacao da mesma, com o uso de vari vel atributo, ou com funcoes especcas que tem como par metro o ID da a a thread a ser manipulada. Para denir os atributos no momento da criacao de uma thread, devemos inicia lizar uma vari vel de atributos. Essas vari veis e as funcoes que as manipulam possuem a a o como pr xo pthread attr, sendo o tipo atributo pthread attr t. O c digo abaixo mostra e a inicializacao de uma vari vel atributo. a pthread_attr_t attr; pthread_t thread; /* ... */ pthread_attr_init (&attr); /* Definico de alguns comportamentos especiais */ a pthread_create (&thread, &attr, &thread_f, thread_param); pthread_attr_destroy (&attr); A funcao pthread attr init inicializa a vari vel atributo, denindo seus valores a para o padr o. Ap s criada e ajustados os valores necess rios, a vari vel atributo precisa a o a a ser passada como segundo argumento para a funcao pthread create para ser corretamente utilizada. A funcao pthread attr destroy remove todos os par metro denidos, evitando a assim que um futuro uso descuidado da vari vel atributo gere problemas. a 2.2.1. Separacao das Threads Dentre os comportamentos a serem modicados, o mais frequente deles e a separacao da thread. Traducao do ingl s detach, separar uma thread signica tornar sua execucao e e nalizacao independente das outras threads. Quando temos uma thread n o separada, a seu valor de retorno permance alocando recursos at que alguma outra thread recolha-o e em meio a chamada pthread join. Numa thread separada, os recursos s o dealocados a automaticamente, como seria feito com um processo que retorna. Contudo, ap s sepao rada, uma thread n o mais pode ter seu valor de retorno recuperado por uma chamada a a pthread join. Uma atualizacao do excerto de c digo mostrado anteriormente demonstra o o uso do pthread attr setdetachstate:

pthread_attr_t attr; pthread_t thread; /* ... */ pthread_attr_init (&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); pthread_create (&thread, &attr, &thread_f, thread_param); pthread_attr_destroy (&attr); Ou seja, todas as threads que recebam como atributo de criacao a vari vel attr ir a a ser criada separada das demais. Outra maneira de separar uma thread e chamar a funcao pth read detach(pthread t*). O primeiro argumento e a thread a qual se deseja separar. Nesse ponto cabe introduzir a funcao pthread self(). Essa funcao retorna o identicador da thread que a chama. Portanto, caso desejemos em determinado ponto do uxo de execucao separar uma thread das demais, fazemos: /* cdigo, cdigo, cdigo... */ o o o pthread_detach(pthread_self()); /* cdigo, cdigo, cdigo... */ o o o e pthread detach pode ser chamada por qualquer outra thread. Por m, deve-se estar ciente que misturar o uxo de execucao das diversas threads pode ter efeitos inesperados. 2.2.2. Escopo de Execucao No PThreads, os novos uxos de execucao podem rodar em dois escopos distintos. No primeiro deles, o escopo do sistema, as threads ir o competir como um processo comum a pelos recursos do sistema. Ou seja, caso voc tenha um processo com quatro threads e de escopo de sistema e um outro processo com apenas uma thread, o tempo de processamento ser dividido como se o sistema operacional estivesse escalonando 5 processo a diferentes; J no escopo do processo, as threads ir o competir internamente pelo tempo a a de processador dado ao processo que as criou. O comportamento descrito acima pode ser denido apenas durante a criacao da thread usando uma vari vel atributo e passando-a como par metro para a a a funcao pthread attr setscope. Duas macros denem os comportamentes: PTH READ SCOPE SYSTEM e PTHREAD SCOPE PROCESS. O c digo abaixo apresenta o uma atualizacao da funcao main do nosso primeiro exemplo para que o mesmo utilize-se do escopo do sistema: /* includes e funco print */ a int main (int argc, char** argv) { int i;

pthread_attr_t attr; pthread_t threads[argc]; pthread_attr_init(&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

for (i = 1 ; i < argc ; i++) pthread_create(&threads[i], &attr, print, (void*)argv[i] return 0; } Apesar de n o apresentar uma mudanca brusca no comportamento do programa, a em situacoes onde o processamento e mais necess rio essa mudanca se mostra signica a tiva.

2.2.3. Escalonamento Escalonar um processo signica agendar tempo de processamento e outros recursos, tais como de entrada/sada, para a execucao do processo. Apesar de ser usada a palavra pro cessos, o conceito tamb m e aplic vel as threads, bastando apenas fazer uma substituicao e a dos termos. Os algoritmos de escalonamento s o a base da tomada de decis o de qual tha a read/processo ir executar caso hajam mais de uma thread/processo pronto para tal. No a descrito at agora, a unica funcao que altera o comportamento do escalonador e pthe read attr setscope, a qual dene o escopo de execucao de uma thread. Al m do escopo, existem outros tr s atributos que denem as tomadas de decis o e e a de escalonamento das threads. Um dos mais interessantes s o as prioridades das threa ads. Esse atributo permite denir, quando h mais de uma thread bloqueada, qual ser a a a pr xima a executar. O mecanismo e simples: a thread de maior prioridade possui maior o preced ncia. e As polticas de escalonamento permitem a denicao de diferentes algoritmos para o controle da ordem de execucao das threads, aproveitando-se tamb m das prioridades e denidas pela funcao pthread attr setschedparam. A primeira poltica, a First-in First out, leva o escalonador ao comportamento quem chegar primeiro leva. Ou seja, dada duas threads que est o concorrendo a uma mesmo recurso. Caso possuam a mesma prioridade, a a que chegou primeiro a la de escalonamento levar a melhor. Caso as duas threads a possuam prioridades distintas, a de maior valor levar a melhor. A macro que dene esse a comportamento por parte do escalonador e SCHED FIFO; na segunda poltica, RoundRobin, as threads de igual prioridade ser o escalonadas segundo o algoritmo de mesmo a nome, podendo algumas delas sofrer por preempcoes. A macro utilizada para esse com portamento e a SCHED RR; Existe tamb m uma terceira poltica, denida pela macro e SCHED OTHER, usada para sinalizar que as threads n o mais precisam de uma poltica a especca de escalonamento.

O mais simples de se compreender, a heranca de escalonamento, dene para a thread que ser criada se ela deve herdar os par metros e as polticas de escalonamento a a de sua thread criadora ou se deve obedecer aos valores presentes na vari vel atributo. A a macro PTHREAD INHERIT SCHED diz a thread rec m criada que use os par metros e e a a poltica de escalonamento de sua thread criadora. Ou seja, ignora qualquer chamada as pthread attr setschedparam e pthread attr setschedpolicy; Por outro lado, a macro PTHREAD EXPLICIT SCHED dene que a nova thread dever utilizar os valores presentes a na vari vel atributo. a 2.3. Sincronizacao O modelo de paralelismo de usado pelas threads e comumente chamado de m ltiplos u processadores de mem ria compartilhada. Em outras palavras, toda a regi o de mem ria o a o e acessvel por qualquer thread. Apesar de trazer vantagens quanto ao acesso dos dados por diversas threads pois evita o uso de mecanismo de comunicacao entre processos, essa liberdade do acesso a mem ria pode trazer, quando m utilizados, diversos problemas um o a tanto quanto difceis de diagnosticar. A maioria desses problemas s o ocasionados pela a n o atomicidade das operacoes realizadas nos uxos de processamento. a Como um exemplo simples podemos usar o acesso concorrente a uma estrutura de dados din mica (aqui ser uma pilha). O c digo segue abaixo: a a o #include <pthread.h> #include <stdio.h> #include <stdlib.h> #define TRUE #define FALSE 1 0

#define MAX_THREADS 10 typedef struct node { void *valor; struct node *proximo; } No; typedef struct stack { No *no; int qtde; } Pilha; Pilha *pilha; Pilha* NovaPilha () { Pilha *nova = (Pilha *) calloc (1, sizeof(Pilha)); if (nova) { return nova; }

return NULL; } int Empilhar (Pilha *p, void *valor) { No *no; if (!p) return FALSE; no = (No *) calloc (1, sizeof(No)); if (no) { no->proximo = p->no; p->no = no; no->valor = valor; p->qtde++; return TRUE; } return FALSE; } No* Desempilhar (Pilha *p) { if (!p) return FALSE; if (p->no) { No *no = p->no; p->no = no->proximo; p->qtde--; return no; } return NULL; } void *threadfa (void *data) { long valor; while (TRUE) {

valor = random (); fprintf (stderr, "Empilhando %ld\n", valor); Empilhar (pilha, (void *) valor); } return NULL; } void *threadfb (void *data) { No *no; int valor; while (TRUE) { no = Desempilhar (pilha); if (no) { fprintf (stderr, "Desempilhando %ld\n", (long) no->valor); free (no); } } return NULL; } int main () { pthread_t threads[MAX_THREADS]; int i; pilha = NovaPilha(); for (i = 0 ; i < MAX_THREADS / 2 ; i++) pthread_create (&threads[i], NULL, threadfa, NULL); for ( ; i < MAX_THREADS ; i++) pthread_create (&threads[i], NULL, threadfb, NULL); pthread_join (threads[0], NULL); } Apesar de correto do ponto de vista sequencial, em ambientes multithreads o nosso c digo apresentar no mnimo tr s erros: Segmentation Fault, Double Free e o a e alocacao excessiva de mem ria (Aborted). o O terceiro erro n o nos interessa muito, mas vale citar sua causa: caso as decis es a o de escalonamento sejam tais que as uxos de execucao que tenham como funcao thre adfa rodem muito mais vezes que os outros uxos, o n cleo julgar que a aplicacao est u a a fazendo mal uso dos recursos de mem ria e a abortar . o a

Tamb m causados pelas decis es de escalonamento, os dois primeiros erros poe o dem ser evitados com o uso de mecanismos de controle de concorr ncia. Por m, antes de e e conhecer tais mecanismos, vale primeiramente entender a causa dos erros. Pelo c digo podemos ver que teremos M AX T HREADS/2 uxos executando a o funcao de empilhamento, threadfa; e M AX T HREADS/2 uxos executando a funcao de desempilhamento e liberando os dados, threadfb. Consideremos o programa executando. Digamos que j tenhamos alguns poucos a valores empilhados. Agora, consideremos que dois uxos que estejam desempilhamento executem e sejam interrompidos exatamente na quinta linha da funcao Desempilhar (No *no = p->no;). Agora digamos que um desses uxos volte ao processador e seja executado at a nona linha da funcao threadfb (free (no);). Ou seja, depois do retorno da funcao e Desempilhar e da liberacao do endereco presente em no. No cen rio descrito acima, podemos ver claramente que o uxo de execucao que a n o avancou estar trabalhando com um valor de mem ria inconsistente. Portanto, sua a a o o pr xima instrucao (p->no = no->proximo) acarretaria num Segmentation Fault. Agora, analisemos um segundo cen rio. Ainda tendo alguns poucos valores ema pilhados, consideremos que dois uxos, mais uma vez desempilhando, executem e sejam interrompidos na quinta linha da funcao Desempilhar. Agora, digamos que os dois uxos avancem at que a funcao retorne. Nesse cen rio, ambos os uxos liberar o o endereco e a a presente em no, o que ocasionar um Double Free e a execucao ser nalizada. a a Um ultimo, e bem elaborado caso, seria no uso de uma biblioteca de Garbage Collection (GC). Essa bibliotecas permitem ao desenvolvedor uma maior exibilidade na dealocacao de mem ria, tornando desnecess rio o uso de funcoes como o free. Portanto, a o a pr pria biblioteca se encarregaria de liberar os dados. Logo, n o existiria a oportunidade o a para um Double Free. Entretando, teriamos um quarto erro: inconsist ncia dos dados. e Digamos que n uxos de desempilhamento avancem at a quinta linha da funcao Deseme pilhar. Agora, digamos que antes da execucao de cada um desses uxos, um uxo de empilhamento seja executado. Cada um dos valores rec m empilhados seriam perdidos e pois na pr xima instrucao dos uxos de desempilhamento o endereco do topo da pilha o seria ajustado para outro segmento de mem ria. Ou seja, os n novos valores empilhados o seriam colhidos pelo GC. Aos trechos de c digos acessados e modicados concorrentemente, denominamos o secoes crticas. Em outras palavras, todo c digo que necessita do uso de mecanismo de o sincronizacao para garantir sua integridade. Alguns podem argumentar que diversas vericacoes resolveriam os problemas. Por m, j e matematicamente provado que, dado um conjunto de instrucoes que n o e a a utilizam diretivas de sincronizacao e que realizam acessos concorrentes a segmentos de mem ria, existe um conjunto de decis es de escalonamento que levam o sistema a um o o estado inconsistente.

2.3.1. Vari veis de Exclus o Mutua a a Como sugerido pelo nome, essas vari veis garantem que apenas um dos uxo de proa cessamento tenham acesso a uma secao crtica. Elas realizam seu trabalho usando uma t cnica de bloqueio com espera ociosa. Para tal comportamento e necess rio o uso de e a duas funcoes, lock e unlock. Antes de entrar numa secao crtica, o uxo corrente precisa pedir passagem a vari vel de exclus o m tua atrav s da funcao lock. Caso nenhum outro uxo esteja nessa a a u e o crtica, o uxo corrente ganha passagem. Esse processo e comumente chamado de seca Aquisicaoda vari vel; Caso outro uxo esteja na vez, o uxo corrente permanece blo a queado at que seja chamada a funcao unlock na vari vel de exclus o m tua em quest o. e a a u a Para casos onde e prefervel fazer outro processamento ao inv s de esperar na vari vel e a de exclus o m tua, existe a funcao trylock. Ela testa primeiramente a vari vel, e caso j a u a a esteja bloqueada, ela sinaliza ao uxo atrav s de seu c digo de retorno. Caso contr rio, e o a o uxo entra na secao crtica. Para sinalizar a sada da secao crtica, o uxo de processa mento deve chamar a funcao unlock, num processo chamado de liberacaoda vari vel. a No POSIX threads, o tipo das vari veis de exclus o m tua s o chamados de ptha a u a a read mutex t. As funcoes de lock, unlock e trylock s o chamadas de pthread mutex lock, pthread mutex unlock e pthread mutex trylock. Apartir de agora abreviaremos vari veis a de exclus o m tuapara mutex(do ingl s, MUTual EXclusion). Antes de utilizar uma a u e mutex e necess rio inicializ -la com a funcao pthread mutex init. a a Como dito pelo cientista computacional Edsongley, n o existe documentacao mea lhor que c digo fonte, vamos a um r pido e abstrato exemplo de uso de mutexes: o a /* Em um dos fluxos */ pthread_mutex_t mutex; pthread_mutex_init (&mutex, NULL); /* Nos fluxos que necessitam de sincronizacao */ pthread_mutex_lock (&mutex); /* seco crtica */ a pthread_mutex_unlock (&mutex); Um exemplo, mais uma vez abstrato, de uso do trylock seria: if (!pthread_mutex_trylock (&mutex)) { /* seco crtica */ a pthread_mutex_unlock (&mutex); } else { /* processamento alternativo */ } Estando o funcionamento b sico explicado, vamos agora ao nosso c digo de a o acesso concorrente a pilha. Primeiramente precisamos vericar quais pontos realmente precisam de sincronizacao. A maneira mais simples e r pida seria sincronizar a chamada das funcoes a

Empilhar e Desempilhar, bastando apenas inserir uma mutex global e adicionar as chamadas a pthread mutex lock e pthread mutex unlock nas funcoes threadfa e threadfb. Ficaria mais ou menos assim: /* ... */ #define MAX_THREADS 20 pthread_mutex_t mutex; /* ... */ void *threadfa (void *data) { long valor; while (TRUE) { valor = random (); fprintf (stderr, "Empilhando %ld\n", valor); pthread_mutex_lock (&mutex); Empilhar (pilha, (void *) valor); pthread_mutex_unlock (&mutex); } return NULL; } void *threadfb (void *data) { No *no; int valor; while (TRUE) { pthread_mutex_lock (&mutex); no = Desempilhar (pilha); pthread_mutex_unlock (&mutex); if (no) { fprintf (stderr, "Desempilhando %ld\n", (long) no->valor); free (no); } } return NULL; } int main () { pthread_t threads[MAX_THREADS];

int i; pilha = NovaPilha(); pthread_mutex_init (&mutex, NULL); for (i = 0 ; i < MAX_THREADS / 2 ; i++) pthread_create (&threads[i], NULL, threadfa, NULL); for ( ; i < MAX_THREADS ; i++) pthread_create (&threads[i], NULL, threadfb, NULL); pthread_join (threads[0], NULL); } Essa e conhecida como o m todo inocente(do ingl s, naive method). Ela funcie e ona, por m n o e a mais inteligente (eciente). e a Analisando-se bem o c digo podemos ver que existe um grande desperdcio de o tempo usando-se o m todo inocente. Primeiro, impedimos a chamada da funcao: a eme pilhagem dos argumentos e do endereco de retorno tamb m estar sendo sincronizado, o e a que e totalmente desnecess rio!; Segundo, temos uma alocacao din mica ocorrendo na a a funcao Empilhar. Alocacao din mica exige uma troca de contexto para sua execucao. a Trocas de contexto devem ser evitadas ao m ximo; a Portanto, seria mais eciente o controle de concorr ncia ser inerente a estrutura de e dado Pilha. Ou seja, a pr pria estrutura de dados teria uma mutex e o lock e unlock seriam o feitos pelas funcoes Empilhar e Desempilhar. Al m de mais eciente, podemos dizer que e e mais seguro. Imagine uma novo programador entrando na equipe de desenvolvimento e esquecendo que deveria sincronizar as chamadas? Ent o, nosso c digo caria como a o exposto abaixo: typedef struct stack { No *no; int qtde; pthread_mutex_t mutex; } Pilha; Pilha *pilha; Pilha* NovaPilha () { Pilha *nova = (Pilha *) calloc (1, sizeof(Pilha)); if (nova) { pthread_mutex_init (&nova->mutex, NULL); return nova; }

return NULL; } int Empilhar (Pilha *p, void *valor) { No *no; if (!p) return FALSE; no = (No *) calloc (1, sizeof(No)); if (no) { pthread_mutex_lock (&p->mutex); no->proximo = p->no; p->no = no; no->valor = valor; p->qtde++; pthread_mutex_unlock (&p->mutex); return TRUE; } return FALSE; } No* Desempilhar (Pilha *p) { if (!p) return FALSE; pthread_mutex_lock (&p->mutex); if (p->no) { No *no = p->no; p->no = no->proximo; p->qtde--; pthread_mutex_unlock (&p->mutex); return no; }

pthread_mutex_unlock (&p->mutex); return NULL; } Como e de costume encontrar por a, temos uma estrutura de dados do tipo la thread-safe. Deve-se observar que at o acesso a vari vel Pilha->no na instrucao de controle e a de uxo if da funcao Desempilhar foi sincronizado. Lembrem-se que podemos encontrar um conjunto de decis es de escalonamento que possa fazer com que o valor lido esteja o inconsistente logo ap s a vericacao. o Atributos das vari veis de exclus o mutua a a Como devem ter notado, a funcao pthread mutex init recebe dois par metros. O a primeiro e o endereco da mutex que desejamos inicialiar e o segundo, at agora, s apa e o receu como nulo. Esse ultimo serve para que o comportamento das mutex seja alterado conforme a necessidade. Para tal objetivo temos o tipo pthread mutexattr t. Esse tipo seria equivalente ao pthread attr t, s que aplicado as mutexes. o Para usarmos tais vari veis, devemos inicializ -las com a funcao ptha a read mutexattr init. Para destruirmos, usamos a funcao pthread mutexattr destroy. Para utiliz -las, basta passar seus enderecos como segundo par metro da funcao ptha a read mutex init. Abstratamente: pthread_mutexattr_t mutexattr; pthread_mutex_t mutex; /* ... */ pthread_mutexattr_init (&mutexattr); pthread_mutex_init (&mutex); /* Definicao de alguns comportamentos especiais... */ pthread_mutex_init (&mutex, &mutexattr); /* ... */ pthread_mutexattr_destroy(&mutexattr); Veremos agora os comportamentos mais interessantes associados com as vari veis a atributos das mutexes. Ecopo da vari vel de exclus o mutua a a Assim com as threads, as mutexes podem existir em dois contextos diferentes: processo ou sistema. No escopo do processo, a mutex reside num espaco de enderecamento local e somente o processo que criou a mutex pode utiliz -la. J no escopo do sistema, a a a mutex pode ser posta num espaco de mem ria compartilhado e ser utilizada por diversos o processo.

Observe que o escopo de uma mutex s est relacionado com o espaco de o a enderecamento que a mesma reside. Ou seja, para que uma mutex seja usadas por di versos uxos de escopo de sistema criadas por um mesmo processo, ela n o necessita a estar no escopo do sistema!!! A funcao usada para determinar o escopo da mutex e pth read mutexattr setpshared (pthread mutexattr t*, int). O primeiro par metro e a a vari vel atributo; o segundo e o escopo da mutex. PTHREAD PROCESS SHARED tora nar possvel o uso da mutex por diversos processo. J PTHREAD PROCESS PRIVATE a a impedir qualquer tentativa de uso da mutex por outro processo. Vale enfatizar que a comportamentos arbitr rios ocorrer o caso se tente usar uma mutex privada entre diversos a a processos. Por padr o as mutex residem num escopo local. a Explicar como utilizar uma mutex compartilhada entre processos foge do tema desse artigo, mas quem quiser saber mais um pouco e ainda ver uma simples e claro exemplo pode dar uma olhada nas p ginas de manual de (3p) pthread mutexattr init. a Tipo da vari vel de exclus o mutua a a Para entende o por que de se ter diversos tipos de mutexes, faz-se necess rio ena tender o conceito de deadlock. Deadlocks, ou empasses fatais, s o situacoes onde o progresso do uxo de a execucao de uma processo ser interrompido devido ao mesmo permancer aguardando a um evento que nunca ocorrer . a Um exemplo simples de deadlock s o observados com o uso de mutexes. Um a questionamento simples: o que ocorre quando um mesmo uxo de processamento tenta adquirir o controle de uma mutex duas vezes consecutivas sem a liberar? Deadlock! Ou seja, o uxo ser interrompido at que o uxo que est com o controle da mutex a libere. a e a Mas o uxo interrompido e o uxo que deve liberar a mutex! Uma esp cie de paradoxo. e Para evitar problemas relacionados com empasses, foram propostos diversos tipos de mutexes. A saber: R pidas (fast mutex): As mais simples. Uma segunda tentativa de aquisicao numa mutex a desse tipo causar uma situacao de inanicao (o uxo permanecer eternamente a a bloqueado); Recursivas (recursive mutex): Sendo um pouco mais sosticadas, essas mutexes lembram quantas vezes foram adquirias por um mesmo uxo. O uxo deve libera-l a a mesma quantidade de vezes para que outro uxo possa adquiri-l . a Checagem de Erro (error checking mutex): Uma segunda tentativa de adquirir essa mutex retorna o c digo de erro EDEADLK; o Muitas vezes ca a d vida: se temos como evitar deadlocks com o uso de mutex u recursivas ou com checagem de erros, por que usar uma mutex r pida se isso tornar a a o processo de descoberta de erro mais difcil? Uma das respostas seria eci ncia. Os e tipos mais sosticados de mutexes nada mais s o que uma extens o do tipo r pido. Por a a a exemplo, caso n o tenhamos uma implementacao de PThreads com suporte a recurs o, a a podemos faz -lo da seguinte forma: e typedef struct rmutex {

pthread_mutex_t check; pthread_mutex_t lock; pthread_t id; int count; } RMutex; int RMutexLock (RMutex* mutex) { pthread_mutex_lock (&mutex->check); if (mutex->id == pthread_self()) { mutex->count++; pthread_mutex_unlock (&mutex->check); return 0; } pthread_mutex_unlock (&mutex->check); pthread_mutex_lock (&mutex->lock); pthread_mutex_lock (&mutex->check); mutex->id = pthread_self (); mutex->count = 1; pthread_mutex_unlock (&mutex->check); return 0; } int RMutexUnlock (RMutex* mutex) { pthread_mutex_lock (mutex->check); if (mutex->id == pthread_self()) { if (!--mutex->count) { mutex->id = NULL; pthread_mutex_unlock (&mutex->lock); } pthread_mutex_unlock (&mutex->check); return 0; } pthread_mutex_unlock (&mutex->check);

return 1; } N o entraremos na discuss o do c digo, serve apenas para mostrar que o que foi a a o dito acima. Vejam que foi necess rio manter um contador, uma identicacao de qual thread a adquiriu o mutex e ainda uma mutex extra para que as vericacoes b sicas possam ser a feitas. As mutexes com checagem de erro chegam at a ter uma implementacao mais e simples. Enfatizando, essa n o e a implementacao das mutexes recursivas nativa do PTha read, e apenas uma ilustracao que as mutexes mais sosticadas podem derivar das mutexes r pidas. a Enquanto uma mutex com checagem de erro provem apenas um mecanismo simpl rio que n o altera muito o comportamento das mutexes r pidas, as mutexes reo a a cursivas trazem consigo algumas complicacoes. No c digo de exemplo, podemos ver que o a mutex lock s ser liberada quando a vari vel count for igual a zero. Caso n o hajam o a a a liberacoes sucientes, o sistema pode mais uma vez sofrer por inanicao. 2.3.2. Vari veis Condicionais a Digamos que temos uma determinada regi o de c digo que a thread s dever executar a o o a ap s uma determinada condicao se tornar verdadeira. Digamos tamb m que essa condicao o e ser estabelecida por uma outra thread e que a vericacao da mesma envolva o acesso a a uma regi o crtica. Utilizando uma mutex, poderiamos fazer: a while (1) { pthread_mutex_lock (&mutex); if (condicao) { /* faz alguma coisa */ pthread_mutex_unlock (&mutex); return 1; } pthread_mutex_unlock (&mutex); } Apesar de funcionalmente correto, o c digo possui um problema simples: carece o de espera ociosa. Ou seja, as threads que aguardar o a manutencao da condicao ir o a a permanecer executando as instrucoes presentes no loop, evitando que o processador seja alocado para alguma outra thread. E justamente aqui que as vari veis condicionais ena tram. As vari veis condicionais s o uma mecanismo um pouco mais poderoso para a a a sincronizacao das threads. Permitem especicar situacoes mais complexas acerca da execucao das threads, al m de garantir que a espera seja feita no ocio. e

Todavia, as vari veis condicionais est o sempre acompanhadas de mutexes. O a a motivo e a guarnicao de um predicado que dever manter-se durante toda a execucao. Em a outras palavras, como no c digo acima sempre haver uma regi o crtica. o a a O tipo associado com as vari veis condicionais s o o pthread cond t. Antes de a a utiliz -los, como de praxe, e necess ria sua inicializacao em meio ao uso da funcao a a pthread cond init (pthread cond t *, pthread condattr t *) ou atribuir o valor PTHREAD COND INITIALIZER. A primeira forma de inicializacao se mostra necess ria a apenas quando deseja-se modicar o comportamento da vari vel em meio ao uso de uma a vari vel atributo. Caso deseje apenas uma vari vel condicional padr o, a atribuicao e mais a a a eciente. Ap s inicializada, as funcoes pthread cond wait (pthread cond t *, ptho read mutex t *) e pthread cond timedwait (pthread cond t *, pthread mutex t *, struct timespec *) podem ser usadas para aguardar a condicao. Os primeiros argumentos de ambas s o id nticos: uma vari vel condicional j inicializada e uma mutex inicializada a e a a adquirida pela thread atual. A funcao pthread cond timedwait ir bloquear somente en a quanto a hora do sistema for menor que o valor especcado pelo seu terceiro argumento.2 Quando chamadas, as funcoes pthread cond wait e pthread cond timedwait, liberam a mutex passada como segundo argumento. Para sinalizar as threads que est o bloquadas na vari vel condicional s o proa a a vidas duas func es: pthread cond signal (pthread cond t*) e pthread cond broadcast o a (pthread cond t *). O primeiro argumento de ambas as funcoes e a vari vel argumento a qual se deseja sinalizar. As duas possuem uma diferenca sem ntica bastante a a o sucinta: pthread cond signal ir desbloquear uma unica thread, aleat riamente mas levando em consideracao as prioridades associadas a cada thread; por sua vez, pth read cond broadcast ir desbloquear todas as threads que estiverem aguardando pelo a sinal. Ent o, o c digo exemplo dessa sess o pode ser re-escrito da seguinte maneira: a o a pthread_mutex_lock (&mutex); if (!condicao) { pthread_cond_wait (&cond, &mutex); } if (condicao) { /* Faz algo... */ pthread_mutex_unlock(&mutex); } else { pthread_mutex_unlock(&mutex); return -1; } Esse fragmento possui a mesma sem ntica que o anterior, por m, devido ao uso a e das vari veis condicionais, n o e necess rio a perman ncia em um looping innito nem a a a e
Para um melhor detalhamento do http://opengroup.org/onlinepubs/007908799/xsh/time.h.html
2

tipo

struct

timespec

veja

muito menos realizar constantes aquisicoes e liberacoes da mutex. Analisando o c digo, primeiramente vemos que e feita a aquisicao da mutex pela o thread; com a mutex em m os, a thread verica logo em seguida se a vari vel condia a cao possui valor diferente de zero. Caso o valor da vari vel condicao seja zero, ptha read cond wait ir por a thread para escutar na vari vel condicional cond e liberar a a a a mutex para que a thread emisora do sinal de execucao possa modicar os dados. Caso o valor seja diferente de zero ou ap s o retorno de pthread cond wait, o c digo do bloco de o o execucao ir alterar os dados compartilhados entre as threads (/* Faz algo... */) e logo em a seguida liberar a mutex; a As boas pr ticas (e o manual) dizem que mesmo recebendo a sinalizacao de uma a outra thread, deve-se re-avaliar os predicados envolvidos. No nosso caso, mais uma comparacao foi realizada na vari vel condicao. a A thread emissor do sinal de execucao possuir um c digo mais simples, que no a a o nosso exemplo seria algo como o fragmento a seguir: pthread_mutex_lock (&mutex); condicao = facaalgo(); pthread_cond_signal (&cond); pthread_mutex_unlock (&mutex); Ou seja, o emissor precisar adquirir a mutex; alterar os dados compartilhados a de forma a garantir a condicao de execucao, que em nosso exemplo e apenas a vari vel a condicao; sinalizar para as threads que est o aguardando o sinal; e liberar a mutex. a O exemplo mostrado e simples, mas com um pouco de imaginacao e possvel criar diversas condicoes avancadas, podendo at imitaro comportamento de diversos outros e mecanismos de sincronizacao. Os sem foros, por exemplo, s o muito simples de imple a a mentar atrav s de mutexes e vari veis condicionais. Ser apresentado o comportamento e a a dos sem foros a seguir, portanto, quem quiser tentar implement -los, ca como tarefa de a a casa, ehehehe... 2.3.3. Sem foros a Os sem foros trabalham como sinalizadores: seu principal uso e para representar a dispoa nibilidade de um recurso computacional. Por exemplo, seja uma la din mica controlada a por uma mutex e duas threads, uma consumidora dos elementos da la e outra produtora desses elementos. Uma das maneiras de garantir a espera ociosa nesse situacao seria criar uma vari vel condicional e usar o contador de elementos da la como condicao de a execucao. Simples e f cil. Mas como essa e uma situacao recorrente nos programas pa a ralelos/multithread, resolveu-se criar uma estrutura de dados para trata-l : os sem foros. a a Essas estruturas de dados possuem duas funcoes; post e wait. A wait faz com que a thread bloquei at que outra thread sinalize no sem foro, tarefa feita atrav s da funcao e a e post. Em outras palavras: wait espera e post d a largada. a O primeiro passo para utilizar os sem foros e incluir o arquivo de cabecalho sea maphiore.h. As funcoes wait e post, na biblioteca PThreads, est o implementadas como a e int sem wait(sem t *sem) e int sem post(sem t *sem), respectivamente. Existem tamb m

as funcoes sem trywait, que verica se o contador do sem foro est positivo e caso esteja a a decrementa-o mas n o bloqueia caso o contador esteja em zero; e sem timedwait, que a permite especicar um intervalo de tempo no qual a thread pode car bloqueada. Para maiores informacoes sobre ambas, o manual on-line tem uma otima explicacao. Antes de utilizar um sem foro, precisamos inicializ -lo. A funcao respons vel a a a por essa tarefa e a int sem init(sem t *, int, unsigned int). O primeiro argumento e o sem foro a ser inicializado, o segundo e uma ag para indicar se o sem foro dever ser a a a compartilhado entre multiplas threads ou multiplos processos. O valor 0 (zero) indica que o sem foro ser utilizado apenas pelas threads de um mesmo processo; o ultimo a a argumento e o valor inicial do contador do sem foro. E um erro horrvel utilizar um a sem foro selvagem e e bem difcil de visualizar o erro. Tomem cuidado com essa parte. a N o e necess rio um exemplo complexo para entender o uso dos sem foros. a a a Ent o, vai um bem abstrato e simples. a /* Nosso semforo */ a sem_t semaforo; /* Semforos, em sua maioria, so acompanhados a a de mutexes. */ pthread_t mutex; /* Semforo compartilhado entre as threads de a um mesmo processo e com o contador vazio. */ sem_init (&semaforo, 0, 0); pthread_mutex_init (&mutex, NULL); /* Excerto da Thread Produtora */ while (1) { pthread_mutex_lock(&mutex); /* Funco de produco */ a a produzir(); pthread_mutex_unlock (&mutex); sem_post(&semaforo); } /* Excerto da Thread Consumidora */ while (1) { sem_wait(&semaforo); pthread_mutex_lock(&mutex); /* Funco de consumo */ a

consumir(); pthread_mutex_unlock (&mutex); } Bem simples: a thread consumidora vai car esperando no sem foro semaforo e a quando a thread produtora sinalizar nessa vari vel, a thread consumidora ser desboquea a ada. Antes de produzir e antes de consumir, as threads entram numa sess o crtica, usando a a vari vel de exclus o m tua mutex para impedir inconsist ncias. a a u e

3. Problemas Cl ssicos de Sincronizacao a


Visando ilustrar melhor os problemas enfrentados pelos desenvolvedores relativos a sincronizacao entre threads/processos, os diversos pesquisadores da area criaram proble mas no mnimo peculiares. Vou comentar e mostrar a solucao de dois deles: o jantar dos l sofos e o barbeiro dorminhoco. o 3.1. O Jantar dos Fil sofos o Esse problema foi proposto em 1965 por Edsger Dijkstra, uma das maiores mentes da computacao. Ele baseia-se na generalizacao de que os l sofos s fazem duas coisas na o o a a vida: comer e pensar. Mas como nem tudo na vida e t o f cil, o jantar dos l sofos o consiste num suculento e escorregadio espagheti, tanto que, para comer o espagheti, s o a necess rios dois garfos! a A vem a pergunta: como isso pode ser modelado num problema de sincronizacao? De forma bem simples. Considere que est o sentados a mesa N l sofos e que est o a o a disponveis N garfos. Considerando que n o vale usar as m os, podermos ter no m ximo a a a N/2 l sofos comendo ao mesmo tempo. Ainda n o sacou por que esse e um problema o a de sincronizacao? Imagine se cada l sofo, ao mesmo tempo, pegar o garfo a sua direita o e n o solt -lo enquanto aguarda a disponibilidade do garfo a esquerda. Ocorrer o que a a a chamamos de deadlock: cada um dos l sofos segurar um garfo e n o ir solt -lo at o a a a a e que o l sofo a sua esquerda disponibilize outro garfo. S que nenhum outro l sofo o o o soltar seu garfo! Portanto, todos os l sofos morrer o por falta de comida, ou o termo a o a t cnico advindo da biologia, inanicao (starvation em ingl s). N o e necess rio dizer que e e a a o contr rio, pegar os garfos a esquerda e esperar o da direta, tamb m leva a uma situacao a e problem tica. a Existem diversas abordagens que resolve esse problema. Irei focar-me aqui numa mais simples, que apesar de funcional, foje um pouco da solucao ideal. O c digo est o a abaixo. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define ESQUERDA(f) ((f + nfilosofo - 1) % nfilosofo) #define DIREITA(f) ((f + 1) % nfilosofo)

#define MSLEEP 5 #define SLEEPTIME (random()%MSLEEP) typedef enum { COMFOME, PENSANDO, COMENDO, } Filosofo; pthread_mutex_t garfos; Filosofo *filosofos; int nfilosofo; void *ffuncao(void *f) { int fid = (int) f; while (1) { pthread_mutex_lock (&garfos); switch (filosofos[fid]) { case PENSANDO: filosofos[fid] = COMFOME; pthread_mutex_unlock(&garfos); fprintf(stdout, "F[%d]: Estou pensando...\n", fid); sleep(SLEEPTIME); break; case COMFOME: fprintf(stdout, "F[%d]: Estou com fome... Vou tentar pegar os garfos!\n", fid); if (filosofos[ESQUERDA(fid)] == COMENDO) { pthread_mutex_unlock(&garfos); fprintf (stdout, "\tFilosofo %d estah comendo... Nao deu pra mim... :-( \n", ESQUERDA(fid)); } else if (filosofos[DIREITA(fid)] == COMENDO) { pthread_mutex_unlock(&garfos); fprintf (stdout, "\tFilosofo %d estah comendo... Nao deu pra mim... :-( \n", DIREITA(fid)); } else { filosofos[fid] = COMENDO; pthread_mutex_unlock(&garfos);

fprintf (stdout, "\tAeee! Vou encher o bucho! :-)\n"); } sleep(SLEEPTIME); break; case COMENDO: filosofos[fid] = PENSANDO; pthread_mutex_unlock(&garfos); fprintf(stdout, "F[%d]: Enchi o bucho... Hora de voltar a pensar...\n", fid); sleep(SLEEPTIME); break; } } } int main (int argc, char **argv) { int i; pthread_t *t; if (argc > 1) { char **endptr = NULL; nfilosofo = (int) strtol(argv[1], endptr, 10); if (endptr) { fprintf (stderr, "Argumento invlido: %s\n", argv[1]); a return 1; } } else { nfilosofo = 5; } filosofos = (Filosofo *) calloc (nfilosofo, sizeof(Filosofo)); if (!filosofos) { fprintf(stderr, "OS filsofos esto cansados. o a No querem pensar hoje...\n"); a return 1; } t = (pthread_t *) calloc (nfilosofo, sizeof(pthread_t));

if (!t) { fprintf (stderr, "Executar ou no executar ... a ... eis a questo!\n"); a return 1; } pthread_mutex_init (&garfos, NULL); for (i = 0 ; i < nfilosofo ; i++) { filosofos[i] = PENSANDO; pthread_create (&t[i], NULL, ffuncao, (void *) i); } for (i = 0 ; i < nfilosofo ; i++) { pthread_join(t[i], NULL); } return 0; } A principal funcao do nosso c digo e a ffuncao. Ela e a funcao encarregada por o manter consistente os estados dos l sofos, que por sua vez, est o representados pela o a enum losofo. De acordo com o c digo, podemos ver que os l sofos poder o estar em o o a tr s estados distintos: COMFOME, PENSANDO e COMENDO. e No incio da execucao, todos os l sofos s o postos no estado PENSANDO, o a como visto na linha 101. Ao iniciar a funcao ffuncao, os l sofos s o postos no es o a tado de FOME. Quando nesse estado, o l sofo que est executando observa o l sofo o a o a sua esquerda e em seguida o l sofo a direta. Caso ambos estejam PENSANDO ou o COMFOME, o l sofo atual entra no estado de COMENDO. Caso contr rio, o l sofo o a o continua no estado de COMFOME e espera um tempo arbitr rio para tentar novamente. a Observe que a vari vel losofo, lida e modicada concorrentemente por todas as threads, a est protegida pelo mutex garfos. a Notem tamb m que e importante liberar a mutex t o logo o trabalho necess rio e a a seja realizado. Por exemplo, no estado COMFOME, a mutex e liberada t o logo o pr ximo a o estado do l sofo possa ser inferido. o O Andrew Tanenbaum, famoso por criar o Minix, apresenta em seu livro Sistemas Operacionais Modernos uma solucao bem mais sosticada, utilizando espera ociosa e tudo o mais. Recomendo a todos dar uma olhada. A Wikip dia, em ingl s, tamb m tem e e e algumas solucoes diferentes da mostrada aqui. Recomendo-a tamb m. e 3.2. O Barbeiro Dorminhoco Atribudo tamb m ao Edsger Dijkstra, o problema do barbeiro dorminho tamb m apre e e senta uma inusitada analogia. Dessa vez, o problema envolve um barbeiro que sempre aproveita o tempo livre para tirar uma soneca, mas n o se importa por ser acordado pelos a clientes.

Nossa miss o nesse problema e garantir que t o logo quanto chegue, o cliente seja a a atendido pelo barbeiro. Caso o barbeiro esteja ocupado, os novos clientes se sentar o nas a cadeiras de espera at que sejam atendidos. Se n o houver nenhuma cadeira disponvel, e a os novos clientes v o embora. Se n o houver clientes, o barbeiro vai tirar uma soneca. a a O problema mais simples, onde existe apenas um barbeiro, e conhecido tamb m e como o problema do barbeiro dorminhoco simples. Nossa implementacao ser baseada a numa vers o mais sosticada, com N barbeiros, que apesar de tudo a solucao e t o f cil a a a quanto a do problema simples. Os clientes chegar o a cada 1 segundo, com P % de a probabilidade de entrarem na barbearia. O c digo est abaixo. o a #include #include #include #include #include #include <pthread.h> <stdio.h> <stdlib.h> <unistd.h> <signal.h> <errno.h>

#include <semaphore.h> int nlugares; int nbarbeiros; int probabilidade; int dlugares; sem_t slugares; pthread_mutex_t mlugares; void *fbarbeiro (void *b) { int bid = (int) b; int q; while (1) { if (sem_trywait (&slugares) == -1) { if (errno != EAGAIN){ perror("Erro"); break; } fprintf (stdout, "Barbeiro[%d]: No tem nenhum a cliente...\n\tVou dormir... zZzzZzZ...\n\n", bid); sem_wait(&slugares); } pthread_mutex_lock (&mlugares); q = --dlugares;

pthread_mutex_unlock (&mlugares); fprintf (stdout, "Barbeiro[%d]: Atendendo cliente...\n\t Ainda existem %d clientes na fila.\n\n", bid, q); sleep (random() % 5); } return NULL; } void gerarclientes() { int q; int lugardiponivel; sleep (2); while (1) { if ((random() % 100) < probabilidade) { pthread_mutex_lock (&mlugares); if (dlugares < nlugares) lugardiponivel = 1; else lugardiponivel = 0; if (lugardiponivel) { q = ++dlugares; sem_post(&slugares); } pthread_mutex_unlock (&mlugares); if (lugardiponivel) { fprintf (stdout, "Um novo cliente chegou...\n\t Ele est aguardando sua vez.\n\n"); a } else { fprintf (stdout, "Um novo cliente chegou...\n\t O salo est lotado. Ele vir outro a a a dia.\n\n"); } fprintf (stdout, "Existem %d clientes na fila e %d lugares disponveis.\n\n", q, nlugares - q);

} sleep(1); } } int bloquearsinais () { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGQUIT); sigaddset(&set, SIGTERM); if (pthread_sigmask(SIG_BLOCK, &set, NULL)) { fprintf (stderr, "No foi possvel mascarar os sinais!"); a return 1; } return 0; } int liberarsinais () { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigaddset(&set, SIGQUIT); sigaddset(&set, SIGTERM); if (pthread_sigmask(SIG_UNBLOCK, &set, NULL)) { fprintf (stderr, "No foi possvel mascarar os sinais!"); a return 1; } return 0; } #define clp(argv,var) \ {\ char **endptr = NULL;\ var = strtol (argv, endptr, 10);\ if (endptr) {\ fprintf (stderr, "Argumento invlido: %s\n", argv);\ a

return 1; \ }\ } int processarlc (int argc, char **argv) { if (argc > 1) { clp(argv[1], nlugares); } else { nlugares = 3; } if (argc > 2) { clp(argv[2], nbarbeiros); } else { nbarbeiros = 1; } if (argc > 3) { clp(argv[3], probabilidade); } else { probabilidade = 80; } return 0; } int main (int argc, char **argv) { int i; pthread_t *barbeiros; if (bloquearsinais() || processarlc(argc, argv)) { return 1; } if (sem_init (&slugares, 0, 0)) { fprintf (stderr, "Erro inicializando semforo\n"); a return 1; } pthread_mutex_init (&mlugares, NULL); dlugares = 0; barbeiros = (pthread_t *) calloc (nbarbeiros, sizeof(pthread_t)); if (!barbeiros) {

fprintf (stderr, "Nenhum barbeiro vir trabalhar a hoje...\n"); return 1; } for (i = 0 ; i < nbarbeiros ; i++) { fprintf (stdout, "O barbeiro %d chegou para trabalhar.\n", i); pthread_create (&barbeiros[i], NULL, fbarbeiro, (void *)i); } liberarsinais(); gerarclientes(); return 0; } Esse programa recebe at tr s argumentos. O primeiro deles e a quantidade de e e clientes que poder o esperar, o n mero de barbeiros e a probabilidade do cliente entrar na a u loja. Portanto, para executar o programa com 3 lugares, 2 barbeiros e uma probabilidade de 90%, basta fazer: $ ./barbeiro 3 2 90 Em nossa solucao, a quantidade de clientes aguardando ser representado pela a vari vel dlugares (disponvel lugares). Sei que cou um pouco contra-intuitivo, pois seria a mais adequado lugares ocupados, mas d para sobreviver com isso. O acesso a essa a protegido pela mutex mlugares. vari vel e a A geracao de novos clientes e o trabalho do barbeiro foi simplicado ao m ximo a para que o exemplo que bem did tico. Mas para que tudo que bem mais claro, vamos a analisar a fundo as funcoes. A fbarbeiro faz o trabalho dos N barbeiros. A parte principal do c digo se encontra reproduzido abaixo: o if (sem_trywait (&slugares) == -1) { if (errno != EAGAIN){ perror("Erro"); break; } fprintf (stdout, "Barbeiro[%d]: No tem nenhum a cliente...\n\tVou dormir... zZzzZzZ...\n\n", bid); sem_wait(&slugares); } Na primeira linha, tentamos decrementar o sem foro slugares, o qual ser usado a a para sinalizar a chegada de um novo cliente, com o uso da funcao sem trywait. O motivo

para tal e que com ela podemos tentar decrementar o sem foro e realizar uma acao suba sequente caso n o seja possvel faz -lo no momento. Nossa acao, para nosso exemplo, a e e por o barbeiro para dormir, que n o passa de imprimir uma mensagem na tela e p -lo a o para esperar no sem foro com a funcao sem wait. Ent o, enquanto n o chegar um novo a a a cliente, nossos barbeiros esperar o sentados e dormindo, pois ningu m e de ferro! a e Assim que e acordado, o barbeiro procura fazer seu trabalho: pthread_mutex_lock (&mlugares); q = --dlugares; pthread_mutex_unlock (&mlugares); Simplesmente decrementamos a vari vel dlugares e guardamos seu valor atual na a vari vel q para apresentar uma mensagem simples na tela. Lembrem-se, quando numa a regi o crtica, devemos nos concentrar no trabalho a ser realizado e liberar a mutex t o a a cedo quanto possvel! Outras threads estar o esperando para executar. a O trabalho realizado pela thread principal, gerar clientes, tamb m e bem simples. e Ela somente incrementa a vari vel dlugares a sinaliza no sem foro slugares. Mas para os a a clientes n o carem chegando o tempo todo e n o se possa observar a acao de dormir, e a a utlizado um valor rand mico para determinar se um novo cliente entra na loja ou n o. o a Uma parte interessante do c digo diz respeito a recepcao de sinais advindos do o sistema operacional. As funcoes bloquearsinais e liberarsinais se encarregam da tarefa de evitar que alguns sinais cheguem as threads e liberar esses sinais para outras threads, respectivamente. O motivo para uso de tal abordagem e que o sistema operacional muitas vezes atrapalha o funcionamento dos sem foros, enviandos sinais que as threads n o est o a a a preparadas para tratar, o que ocasiona alguns incovenientes. As outras funcoes s o apenas aspectos n o-funcionais, que n o alteram em nada a a a o prop sito de passar uma nocao mais pr tica dos mecanismos de sincronizacao. o a

4. Recomendacoes de Leituras
Apesar de n o estarem explcitas no texto, essas refer ncias foram muito importantes para a e a consolidacao do mesmo: 1. Mitchel, Oldham e Samuel. Advanced Linux Programming. New Riders, 2001. www.advancedlinuxprogramming.com 2. Marshall. Futher Threads Programming: Thread Attributes. http://www.cs.cf.ac.uk/Dave/C/node30.html 3. Maier. Threads Scheduling with pthreads under Linux and FreeBSD. http://www.net.t-labs.tu-berlin.de/ gregor/tools/pthread-scheduling.html 4. Barney. POSIX Threads Programming. https://computing.llnl.gov/tutorials/pthreads/ 5. Lampkim. Pthreads: semi-FAQ Revision 5.2. http://www.cognitus.net/html/howto/pthreadSemiFAQ.html 6. Wikipedia. Dining Philosopher Problem. http://en.wikipedia.org/wiki/Dining philosophers problem 7. Wikipedia. Sleeping Barber Problem. http://en.wikipedia.org/wiki/Sleeping barber Recomendo a leitura de cada uma delas. A refer ncia 1 e a que mais recomendo. e Ela possue vers o impressa. Quem zer pleno uso dos conhecimentos presentes aqui e no a ALP, comprem o livro! E muito bom e vale cada centavo!

As pr ximas refer ncias n o tratam diretamente sobre processamento multithread o e a ou paralelo, por m vale a refer ncia pois apresentam conceitos importantes: e e 1. Tanenbaum. Organizacao Estruturada de Computadores, 5 ed. Prentice Hall, 2007. Nota: captulo 8, Arquitetura de Computadores Paralelos. 2. Tanenbaum. Sistemas Operacionais Modernos, 2 ed. Prentice Hall, 2003.

Vous aimerez peut-être aussi