Professional Documents
Culture Documents
Resumo
A popularizao de novas categorias de aplicaes, em particular a disponibilidade de programas que executam a partir de browsers, muitas vezes obriga o desenvolvedor a lidar com aspectos de concorrncia antes restritos a comunidades bem especficas. Os conceitos bsicos da rea em geral so apresentados como parte de um curso de sistema operacional ou banco de dados, mas neste minicurso enfocamos o assunto tendo como motivao seu uso para o desenvolvimento de programas concorrentes que executam na internet como Applets Java.
Abstract
The increasing popularity of new categories of applications, particularly applications in the internet that are accessed by browsers, many times requires that the developer deal with concurrency aspects so far usually restricted to specific computer science communities. The basic concepts in the area are usually presented as part of an operating systems or data base course, but in this short course we focus the subject through its use in the development of concurrent programs that execute as Java Applets.
1. Introduo e motivao
A rea de programao concorrente vem sendo explorada h vrias dcadas, sob vrios enfoques. Em muitos desses enfoques o trabalho foi bem sucedido:
problemas bsicos foram definidos e solucionados, aspectos tericos caracterizados e estudados, aspectos de implementao largamente experimentados e analisados. Em geral, o impacto dos resultados obtidos por esses estudos estavam confinados a comunidades especficas, estando o desenvolvedor de sistemas razoavelmente protegido dos desafios e problemas da rea, a menos que desenvolvesse software bsico (sistemas operacionais, gerenciadores de bancos de dados) ou atuasse diretamente em reas especficas como computao paralela ou sistemas de tempo real. Mas novas categorias de aplicaes, com crescente impacto na rea de computao, muitas vezes obrigam o desenvolvedor a enfrentar diretamente as questes relativas concorrncia. Conceitos, mecanismos e implementaes antes restritos a livros acadmicos passaram a exercer um papel importante em livros tcnicos direcionados aos desevolvedores de software. Vrios textos recentes [Magee-Kramer 1999, Butenhof 1997, Lea 1997] apresentam formas de lidar com concorrncia do ponto de vista de tecnologias especficas, enquanto outros textos abordam a questo de forma totalmente conceitual, sem abordar diretamente tecnologias recentes e bem difundidas atualmente [Milner 1995, Andrews 1991]. O objetivo deste texto aliar os dois extremos, isto , apresentar uma introduo rea de concorrncia que contemple tanto seu uso imediato no desenvolvimento de aplicativos (em especial, aplicativos para a internet) quanto a formao conceitual bsica comum s vrias vertentes da Cincia da Computao relacionadas a programao concorrente. Muitos dos conceitos e problemas apresentados aqui fazem parte dos currculos usuais de graduao na rea de Cincia da Computao, aparecendo em geral como um item entre vrios a serem abordados nas disciplinas de sistemas operacionais, banco de dados, computao paralela ou computao distribuda. Este mini-curso se diferencia por enfocar os problemas fora dos contextos e requisitos especficos destas disciplinas. O mini-curso foi elaborado de forma a contemplar um pblico que se encaixe de alguma forma nas seguintes premissas: O aluno est interessado no desenvolvimento de aplicativos para a internet (isto , programas que possam ser utilizados a partir de browsers) e disposto a aprender conceitos e mecanismos teis para melhorar a qualidade de tais aplicativos; O aluno j estudou conceitos de concorrncia (por exemplo, no contexto de sistemas operacionais) e gostaria de aplic-los no desenvolvimento de aplicativos gerais; O aluno est interessado em entender o que h por trs do pacote de Threads fornecido pela linguagem de programao Java, ou qual seu propsito geral; aluno gosta de desafios, e est disposto a lidar com programas onde a dificuldade de depurao e anlise bem maior do que a usual.
Alm do contedo idealizado para o pblico alvo acima descrito, foi includo no texto material introdutrio sobre modelagem de sistemas. Esta parte apresentada de forma relativamente independente do restante do material, e foi includa para exemplificar um caminho que sirva de fundao para uma abordagem disciplinada e metdica de programao concorrente. Este mini-curso assume que o aluno tem uma maturidade em programao no nvel de segundo anistas de um curso de graduao em computao, e que ele se sente confortvel com a utilizao de estruturas de dados bsicas como pilha, fila e listas ligadas. Na quase totalidade dos exemplos utilizada a linguagem Java; como no assumido que o aluno domine esta linguagem, os programas so documentados pesadamente, de forma a facilitar a compreenso. Todos os exemplos esto disponveis na home page deste texto (http://www.ime.usp.br/~dilma/jai99/). Neste stio1 voc tambm encontrar atualizaes do texto e um endereo para envio de correes, sugestes ou dvidas. Mais importante: os exemplos l apresentados so mais gerais e ligados programao concorrente para internet de que os aqui desenvolvidos, em funo do espao que ocuparia a incluso, com documentao detalhada, de programas completos cuja funcionalidade fosse um pouco mais interessante. O restante desta seo define informalmente a noo de concorrncia e descreve o tipo de aplicao que motiva o restante do nosso texto A seo 2 apresenta os conceitos bsicos e definies da rea. Na seo 3 abordamos os aspectos prticos da programao concorrente, focalizando no suporte oferecido pela linguagem Java. A seo 4 lida, superficialmente, com os problemas de corretude e desempenho. A seo 5 apresenta aspectos sobre applets, concorrncia e distribuio, mas as aplicaes so bem simples (exemplos mais interessantes esto disponveis na pgina deste texto). A ltima seo do texto descreve brevemente aspectos de modelagem de sistemas concorrentes.
durante um perodo de tempo e depois espere sua prxima chance de avanar na computao. A programao concorrente lida com atividades que, embora executem separadamente, estejam relacionadas de alguma forma. Em alguns casos, as atividades esto relacionadas por terem um objetivo comum, por estarem cooperando para solucionar um problema. Em outros casos, a relao entre as atividades mais frouxa, estas podendo ser totalmente independentes em seus propsitos, mas precisando compartilhar os recursos pelos quais competem por estarem executando em um ambiente comum.
Mas os requisitos de concorrncia no aparecem apenas nas aplicaes que lidam com estmulos externos inerentemente concorrentes. Com o avano da tecnologia das ltimas duas dcadas, cada vez mais vivel a implementao de sistemas de forma distribuda sobre uma rede de computadores. Implementaes distribudas, por oferecerem em geral vrias portas de entrada para o uso do sistema, so obrigadas a lidar com a concorrncia das atividades que executam simultaneamente nos diferentes ns da rede. Algumas vezes as razes que levam opo por uma implementao distribuda ao invs de centralizada servem como argumentos para que se empregue concorrncia mesmo em uma implementao centralizada do sistema. Outra motivao para o uso de concorrncia a busca por solues que permitam que vrias partes de um mesmo problema sejam atacadas em paralelo. Com este enfoque, se a plataforma de execuo dispor de muitos processadores (o que j possvel com estaes de trabalho de custo relativamente baixo), o problema poder ser resolvido em menos tempo, ou pelo menos solues parciais teis ao usurio podero ser rapidamente fornecidas, e incrementalmente refinadas. A diviso de uma tarefa a ser executada em vrias atividades concorrentes que
colaboram para atingir o resultado esperado introduz muitas dificuldades algoritmicas e de implementao. As dificuldades bsicas so abordadas com detalhe neste texto (Sees 2.1 e 2.2). Para algumas novas categorias de aplicaes, que se tornam cada vez mais populares, os aspectos de concorrncia se tornaram particularmente relevantes: aplicaes multmidia. O desenvolvimento de aplicaes que manuseiam som e imagem de vrias fontes vem sendo muito explorada. Aplicaes como vdeofone, vdeo-mail e vdeo-conferncia operam com requisitos de tempo real, isto , uma soluo deve no s obter resultados computacionais corretos, como tambm respeitar restries temporais (chegar aos resultados dentro dos limites impostos pela aplicao). Sistemas de tempo real podem ser de dois tipos: (1) hard, em que o no atendimento a uma restrio temporal significa falha total do sistema ou (2) soft, em que se procura respeitar as restries temporais, mas a falha em uma ou outra restrio de tempo ocasional no tem conseqncias srias, e portanto no constitui uma falha completa do sistema. A rea de tempo real vem sendo estudada h muito tempo, e nela essencial que o gerenciamento de tarefas simultneas seja feito de forma eficiente, escolhendo as tarefas sempre de forma a garantir que limites de concluso das vrias tarefas pendentes sejam respeitados. A popularizao do desenvolvimento de aplicaes multimdia implica na necessidade de ambientes computacionais eficientes e flexveis no gerenciamento de concorrncia, de forma que o desenvolvedor da aplicao multimdia ganhe de graa o controle das restries temporais de sua aplicao. Muitos trabalhos recentes investigam a construo de tais ambientes computacionais [Smart 1997, Eclipse 1998, Rosu 1997], mas o problema est longe de ser resolvido. interfaces baseadas em janelas. Aplicaes com interface via janelas grficas tornam a concorrncia ainda mais evidente para o usurio, j que estas oferecem vrios servios ao usurio via componentes como botes e menus que podem ser acionados quase que ao mesmo tempo, causando a execuo simultnea dos servios solicitados. Por exemplo, um usurio que utilize uma interface grfica para receber e enviar e-mails pode solicitar leitura de novos emails a qualquer momento, inclusive enquanto utilize a interface para compor/enviar novos e-mails ou ler e-mails antigos. A implementao deste aplicativo de e-mail deve ter sido projetada de forma a lidar com as tarefas de envio e recebimento de e-mail executando simultaneamente e competindo por recursos (por exemplo, pelo repositrio de mensagens recebidas).
servidores de informao na Teia2. Cada usurio da Teia pode operar sequencialmente, isto , visitando uma pgina de cada vez. Mas o servidor (programa que gerencia o envio de pginas solicitadas) poder receber vrios pedidos concorrentes de pginas. Conforme o uso da Teia v aumentando, veremos cada vez mais grandes bancos de dados acessveis via browsers, aumentando o potencial para acesso concorrente dos mesmos. Mais recentemente, vem aumentando a presena de animao nas pginas da Teia; processos concorrentes poderiam ser utilizados do lado do cliente de forma a agilizar a animao e permitir que o usurio interaja com a pgina (preenchendo formulrios, por exemplo) enquanto a animao progride. middleware. Um enfoque amplamente utilizado para lidar com heterogeneidade em sistemas distribudos construir uma camada de software acima dos sistemas operacionais heterogneos de forma a apresentar uma plataforma uniforme sobre as quais aplicaes distribudas possam ser executadas. Esta camada o middleware em geral responsvel por gerenciar a execuo concorrente dos vrios componentes do sistema distribudo.
pode especificar que deseja ter seus bilhetes automaticamente enviados para seu email. Bilhetes podem ser enviados para participantes ou para pseudnimos cadastrados, sendo analogamente assinados por participantes ou por pseudnimos. O programa lida com aspectos de segurana, isto , no torna disponvel o repositrio de informaes sobre o amigo secreto. O segundo programa ajuda na organizao de uma festa, permitindo que as pessoas confirmem sua presena ou ausncia, verifiquem quem j confirmou a ida a festa, e especifique qual ser sua contribuio para a festa (que bebida ou comida, de uma lista disponibilizada pelo promotor da festa).
Mais formalmente, a descrio de uma computao em uma linguagem formal [Hansen 1973].
Threads (ou processos em programas concorrentes em geral) devem colaborar entre si por dois motivos: (1) para que a funcionalidade esperada do programa seja atingida e (2) para que eles possam se coordenar quanto ao uso de recursos compartilhados. Esta colaborao feita atravs de comunicao. Por exemplo, se uma aplicao de busca de um usurio chamado Jassei Java em uma dada rede de computadores composta por vrias threads, em que cada thread busca o usurio em um dado subdomnio da rede, se um dos threads encontra o usurio ele deve comunicar este fato para os outros threads, de forma que estes cessem as suas buscas. Se a busca pelo nmero de usurios com o sobrenome Java, as quantidades encontradas pelos vrios threads devem ser somadas para formar o resultado final da aplicao. Por outro lado, se a aplicao exige que toda vez que um usurio com sobrenome Java seja encontrado lhe seja enviado um FAX oferecendo um curso de Java de graa, vrios threads podem requerer simultaneamente o uso da placa de FAX, e devem colaborar para que todos consigam acessoao recurso compartilhado. Comunicao se d atravs do uso de variveis compartilhadas ou do envio de mensagens. Para que a comunicao ocorra, necessrio que os envolvidos estejam prontos em seus postos: um thread pronto para dar a comunicao, outro(s) para receber. Em outras palavras, comunicao exige sincronizao. Duas formas de sincronizao ocorrem em programas concorrentes [Andrews 1991]: excluso mtua e sincronizao condicional. Excluso mtua tem como objetivo sincronizar threads de forma a garantir que determinadas partes do cdigo sejam executadas por no mximo um thread de cada vez, servindo assim para evitar que recursos compartilhados sejam acessados simultaneamente por vrios threads. O desenvolvedor do aplicativo deve identificar quais so as partes do cdigo que exigem este tipo de sincronizao (isto , quais so as regies crticas do programa) e utilizar alguma primitiva de comunicao entre threads disponvel em seu ambiente para delinear o incio e o fim da regio crtica onde se deseja excluso mtua. Sincronizao condicional permite que o programador faa com que um thread espere uma dada condio ser verdadeira antes de continuar sua execuo. Com esta forma de controle do andamento de um thread possvel orquestrar o andamento da computao, por exemplo garantindo que um thread espere pela informao computada por um outro thread antes de progredir com sua computao. O desafio para o desenvolvedor est em identificar onde colocar sincronizaes condicionais de forma a dirigir corretamente o andamento coletivo dos threads, e tambm escolher as condies lgicas adequadas a cada sincronizao condicional. Na seo 4 abordaremas as primitivas disponveis para criao e sincronizao de threads em Java e em C/UNIX, e como programar com elas. A fim de esclarecer
como o suporte para threads nestes ambientes funciona, estudaremos como algumas primitivas de sincronizao baixo nvel funcionam e foram implementadas. O objetivo no cobrir detalhes, e sim evidenciar as dificuldades encontradas em implementar e utilizar tais suportes para sincronizao.
Por exemplo, ao especificar a espera por uma interrupo de hardware, indica-se uma sincronizao com um hardware. J com interrupes de software, pode-se especificar sincronizao entre programas ou atividades de um programa. Estes mecanismos podem ser generalizados de forma a cobrir os casos em que se espera qualquer evento oriundo de uma dada atividade, ou um evento especfico oriundo de qualquer atividade. As duas primitivas so teis para expressar sincronizao condicional, mas dependendo da forma com que so implementadas, seu uso pode se tornar invivel, como exemplificamos a seguir. Considere uma aplicao formada por duas atividades: (1) um thread que acha nmeros primos, isto , consecutivamente procura o prximo inteiro que seja primo e (2) um thread que notifica por e-mail o chefe do laboratrio de pesquisa em primos quando um novo primo encontrado. Os dois threads precisam sincronizar, isto , o que busca primos deve notificar que um primo foi encontrado, e o thread que envia e-mail deve aguardar por tal notificao (esperar at que uma condio, o encontro de um nmero primo, seja verdadeira).
Thread Procura while ( ) { // executa para sempre // busca prximo primo acha_prximo_primo(); // avisa ao outro Thread SIGNAL (Informa); }
A aplicao principal simplesmente criaria os dois threads. Suponha que o thread Procura foi criado e iniciado antes do thread Informa, e que no momento em que Procura executa SIGNAL (Informa), o thread Informa ainda no executou WAIT(Procura). Dependendo da implementao (e este o caso em muitas das implementaes disponveis em ambientes Unix), o sinal enviado por Procura para Informa perdido (neste exemplo, consequentemente o primeiro primo encontrado no causaria o envio de um e-mail). Outra situao anloga surge quando o SIGNAL de Procura faz com que Informa continue sua execuo e execute envia_email_para_chefe(), mas enquanto esta rotina processada por Informa vrios outros primos so encontrados em Procura, com os respectivos SIGNAL (Informa) sendo perdidos, j que Informa ainda no est atingiu novamente WAIT(Procura). Estes problemas podem ser evitados se a implementao de WAIT/SIGNAL garantir que os sinais que cheguem adiantados ao comando WAIT sejam acumulados e entregues para a aplicao que os espera, mas isto tornaria as primitivas mais custosas (trabalho extra de gerenciar que sinais j foram tratados e quais devem ser entregues no prximo WAIT).
Se dois threads esto executando concorrentemente, potencialmente qualquer entrelaamento das instrues dos dois threads pode ocorrer. Por exemplo, considere os threads SomaUm e SomaDois com os seguintes cdigos: SomaUm x = x + 1; SomaDois x = x + 2;
Cada um desses threads tm um nico comando, mas trs instrues : (1) LD x (l x da memria), (2) adiciona constante, e (3) e ST x (escreve o novo valor de x na memria). As duas execues a seguir de um programa composto por estes dois threads so vlidas. A descrio das execues apresentada como uma progresso no tempo que indica a ordem em que as instrues foram executadas. A fim de tornar explcito a quem pertence a instruo executada, a mesma escrita na coluna correspondente ao seu thread. Histrico da Execuo 1: SomaUm LD x SomaDois
Incrementa em 1 ST x LD x
Incrementa em 1 LD x
Incrementa em 2 ST x ST x
O primeiro histrico corresponde a execuo sequencial (sem atividades concorrentes) de SomaUm seguida da execuo sequencial de SomaDois. O segundo histrico corresponde a uma execuo concorrente dos dois threads em que SomaUm comea a execuo e interrompido aps duas instrues. Assumindo que a varivel x como valor inicial zero, ao final da execuo 1 ela armazena o valor 3, enquanto que ao final da execuo 2 x tem o valor 2. Em outras palavras, o programa formado pelos dois threads no tem um resultado determinstico, j que diferentes escalonamentos dos threads (isto , diferentes escolhas sobre qual thread deve progredir em sua execuo) resultam em valores finais diferentes. O mesmo problema ocorre se o programa for executado em uma plataforma com vrios multiprocessadores, pois como os processadores podem ter diferentes velocidades de execuo ou de acesso memria, no se sabe a priori a ordem relativa entre as instrues dos dois threads. Este indeterminismo pode ser evitado se o acesso varivel compartilhada x for feito com excluso mtua, isto , se forem impossibilitadas execues em que mais de um thread esteja executando alguma parte do acesso ao recurso compartilhado (isto , estiver na regio crtica). A regio crtica do exemplo acima bem curta (um comando em linguagem de alto nvel apenas), mas se o recurso compartilhado for uma estrutura de dados complexa (como uma B-rvore ou grafo dirigido), a seo do cdigo que manipula a estrutura pode ser longa e de execuo demorada. O problema a ser resolvido como fornecer a um thread acesso exclusivo a uma estrutura de dados para um nmero arbitrrio de leituras e gravaes. As subsees a seguir discutem solues clssicas para o problema. Observe que alm de garantir o acesso exclusivo, desejvel que solues para o problema tenham as seguintes caractersticas: Se dois ou mais threads esto tentando obter acesso para a estrutura de dados, ento pelo menos um deles vai ter sucesso. Em outras palavras, a execuo do programa no fica congelada: no ocorre deadlock4. Se um thread T est tentando obter acesso exclusivo a um recurso ( como uma estrutura de dados, por exemplo) e os outros threads esto ocupados com tarefas que no se relacionam com o recurso compartilhado, ento nada impede que o thread T obtenha o acesso exclusivo. Isto fica mais claro se pensamos em uma soluo em que esta caracterstica no atingida: suponha o esquema de acesso a uma placa de FAX compartilhada por dois threads em que o acesso exclusivo obtido com a seguinte regra: durante os 30 primeiros minutos de
Dizemos que um conjunto de threads est em deadlock se todos os threads do conjunto esto aguardando eventos que s podem ser gerados por threads pertencentes ao conjunto. Em outras palavras, eles esto bloqueados esperando por algo que no pode ser fornecido por elementos externos ao conjunto.
qualquer hora s o thread 1 pode usar a placa, e nos ltimos 30 minutos s o thread 2 tem acesso a este recurso compartilhado. Com este esquema, temos que se o thread 2 quer acesso s 14:10, ele ter que esperar (pelo menos 20 minutos) mesmo se o thread 1 no estiver utilizando o recurso. Se um thread est tentando conseguir acesso ao recurso, alguma hora ele consegue.
O cdigo acima obviamente continua com problema, uma vez que o teste do valor da varivel flag e sua mudana para 1 no so atmicas. Em uma plataforma com vrios processadores, existe uma chance de que os dois threads fossem executados exatamente ao mesmo tempo, e portanto ambos achassem que a regio crtica estava livre. Mesmo em processadores com um nico processador, o problema continua: possvel que o primeiro thread carregue da memria o valor de flag, compare com 0 e seja interrompido exatamente antes de ter a chance de escrever 1 na varivel (com isso indicando que a regio crtica est ocupada). Assim, o segundo thread ao carregar o valor de flag tambm obteria 0, e teramos os dois threads ativos na regio crtica. Muitos computadores tm alguma instruo que executa atomicamente leitura, teste de valor e escrita. Isto , a instruo executa em um nico ciclo, a operao de (1) verificar se a regio crtica est livre e (2) marc-la como ocupada se este o caso no pode ser interrompida. Um exemplo de tal instruo a Test-And-Set, que tem tipicamente a seguinte forma:
se a booleana indica que a regio est livre { mude a booleana para indicar regio ocupada; pule a prxima instruo; } seno execute a prxima instruo; // em geral uma instruo que desvia o // fluxo para fazer o teste de novo Outra forma de enxergar as implementaes de Test-And-Set disponveis nos hardwares usuais como uma instruo que dadas duas posies de memria, atomicamente copia o contedo da segunda na primeira e coloca 1 na segunda. A entrada na regio crtica ficaria ento: valor_lido = 1; flag = 0; while (valor_lido == 1) { Test-And-Set (valor_lido, flag); // atmico if ( valor_lido == 0 ) { // se estava zero antes de eu pedir para colocar < cdigo da regio crtica > flag = 0; } } Este tipo de soluo, que envolve repetidamente testar se a regio crtica ficou livre, chamada de espera ocupada (busy wait ou spin lock), j que enquanto o thread espera para entrar na regio crtica ele continua gastando a CPU com comparaes e chamadas do Test-And-Set. Alternativamente, o thread poderia ser bloqueado, isto , sair da CPU por algum tempo, voltando para tentar novamente mais tarde (preferencialmente, nos momentos em que a regio crtica foi desocupada e portanto h chance de sucesso na tentativa de obter acesso). Outras instrues de hardware (por exemplo, Compare-And-Swap atmico) tornam possvel o uso de uma varivel para indicar se a regio crtica est livre ou ocupada. Observe, no entanto, que este tipo de soluo exige que a varivel faa parte do espao de endereamento de todos os threads envolvidos, isto , que as atividades concorrentes possam compartilhar posies de memria. Em ambientes com vrios processadores, o compartilhamento de uma varivel como o que foi feito com o Test-And-Set no exemplo acima pode causar tremendos prejuzos de desempenho, pois a cada vez que se escreve 1 em flag (o que feito toda fez que Test-And-Set executada, estando a regio crtica livre ou no) as entradas dos caches correspondentes varivel em cada processador podem ter que ser invalidadas.
Outra deficincia de uma soluo baseada em Test-And-Set a questo de justia: nada impede que um thread consiga entrar sucessivamente na regio crtica, enquanto outro persiste na tentativa sem ter sucesso. Em ambientes com apenas um processador, uma tcnica comum para proibir que a execuo seja interrompida simplesmente desabilitar as interrupes durante a execuo da regio crtica, tornando impossvel que outro thread tenha a chance de executar5. Esta soluo se torna inaceitvel se a regio crtica for longa, pois interrupes importantes (como, por exemplo, as relacionadas entrada e sada) poderiam ser perdidas.
Os algoritmos que solucionam o problema da excluso mtua no so simples de entender. Uma descrio detalhada e bem formalizada de solues presentes na literatura pode ser encontrada em [Andrews 1991]. Nesta seo os algoritmos so discutidos superficialmente; suas implementaes em Java esto disponveis, de forma detalhada e discutida, em http://www.ime.usp.br/~dilma/jai99/. O algoritmo de senha (Ticket algorithm) se baseia no procedimento adotado em grandes aougues, confeitarias ou centrais de atendimento a fim de garantir que os clientes sejam atendidos na ordem de chegada sem que se forme uma fila fsica. H um dispositivo que libera cartes numerados sequenciamente (senhas). O cliente ao chegar se dirige ao dispositivo e retira um nmero, e fica esperando at que seu
5
nmero seja chamado. Se pensamos nesta soluo em um cenrio em que apenas um atendente est disponvel, vemos os clientes competindo pelo atendente da mesma forma em que threads competem pelo acesso regio crtica. Uma implementao dessa idia pode ser feita utilizando-se duas variveis compartilhadas: nmero e prximo, ambas tendo 1 como valor inicial. Cada thread ento teria a seguinte estrutura algoritmica: int minha_vez; // varivel interna ao thread, // no compartilhada. minha_vez = nmero; nmero ++; while (prximo != minha_vez) ; executa_regio_crtica(); // ATMICO ! // espera ocupada // aqui est o cdigo a ser protegido // de execuo simultnea prximo++; // ATMICO: passa posse da regio // crtica pro seguinte Os nmeros distribudos aos threads que desejam entrar na regio crtica no se repetem, e se a obteno de um nmero por um thread e a gerao do nmero seguinte forem feitas de forma indivisvel, ento h garantia de que no mximo um thread por vez tem acesso ao cdigo executa_regio_crtica(). Cada cliente (cada thread) atendido assim que chega a sua vez e h garantia de que todo thread pleiteando acesso vai conseguir alguma hora. Mas a soluo tem um problema: as variveis prximo e nmero crescem ilimitadamente; na prtica se o programa rodar por muito tempo, as variveis atingiriam o limite mximo, em muitas arquiteturas voltando para zero. Isto seria um problema se o nmero de threads esperando pela sua vez excedesse ao maior inteiro armazenvel, pois dois threads estariam esperando com o mesmo nmero de atendimento. Outra questo importante que esta soluo exige que alguns trechos (delimitados por um retngulo no cdigo acima) sejam executados atomicamente, podendo ser implementados em mquinas que tenham uma instruo atmica de Fetch-AndAdd6 ou atravs da desabilitao temporria de interrupes nestes pequenos trechos de cdigo. O algoritmo da Padaria (Bakery Algorithm), proposto por Lamport em 1974, aproveita a idia da distribuio de senhas, com a vantagem de no exigir um gerador atmico do prximo nmero e nem a manuteno de uma varivel
6
prximo. O algoritmo mais complicado, mas ilustra uma idia bem til: quebrar empates quando dois threads esto com senhas com o mesmo nmero. A idia do algoritmo tambm vem do mundo real, como no caso do algoritmo com distribuio de senhas. Quando a gente vai a uma padaria ou aougue mais simples, em geral no encontramos um dispositivo para a gente pegar um nmero de atendimento. Simplesmente a gente olha pros lados, observa quem est esperando para ser atendido, e se situa sobre quando seremos atendidos (s quando os que j estavam l tiverem sido atendidos). Em outras palavras, o thread escolhe para si um nmero que seja maior do que qualquer outro nmero que esteja em posse de algum thread esperando pelo acesso, e o thread sabe que est na hora de executar quando o seu nmero for o menor entre todos que estejam esperando. O seguinte pseudo-cdigo descreve o que cada thread faz para entrar na regio crtica, utilizando-se uma vetor de inteiros vez[], onde vez[t] armazena 0 se o thread t no est interessado em entrar na regio crtica ou armazena um nmero > 0 que representa seu nmero de atendimento. // incio da tentativa de entrar na regio crtica // t um identificador nmero do thread vez[t] = mximo(vez) + 1; //atribuio de nmero de atendimento // espera pela sua vez
Obviamente, se as aes identificadas por retngulos acima no forem executadas com exclusividade pelo thread, mais de um thread pode acabar escolhendo o mesmo nmero de atendimento. O algoritmo permite que isto acontea, ou seja, que mais de um thread receba um mesmo nmero de atendimento, mas impe um desempate entre os que tm o mesmo nmero, de forma que apenas um deles acabe entrando na regio crtica.O desempate se d atravs do nmero identificador de cada thread. Ou seja, ao invs de comparar vez[a] > vez[b], comparamos (vez[a], a) > (vez[b], b) onde esta expresso verdadeira se vez[a] > vez[b] ou (vez[a] = vez[b] e a> b). O algoritmo fica:
// incio da tentativa de entrar na regio crtica // t um identificador nmero do thread vez[t] = mximo(vez) + 1; // atribuio de nmero de atendimento // OK se nmeros repetidos forem fornecidos para todo thread j que no seja este thread t { enquanto (vez[t] 0 e (vez[t], t) > (vez[j], j) ; executa_regio_crtica(); vez[t] = 0; // no est mais interessado na regio crtica // espera pela sua vez
Para perceber porque o algoritmo funciona, isto , porque no existe uma situao em que dois threads executam ao mesmo tempo executa_regio_crtica() preciso entender como funciona o critrio de desempate contido no retngulo pontilhado acima. A forma de desempate entre vrios threads que por acaso receberam o mesmo nmero de atendimento se baseia no algoritmo de Petersen (tambm conhecido como Tie-Breaker), que tambm um algoritmo que pode ser usado para o problema da excluso mtua (j que se quer desempatar entre vrios threads que desejam acesso regio crtica). A idia um desempate em fases: se existem n threads no programa, o algoritmo tem n fases. Em uma primeira fase, n threads executam vez[t] 0 e (vez[t], t) > (vez[j], j), mas mesmo que todos estejam empatados no nmero de atendimento vez[], um deles perde para todos os outros7, resultando em que no mximo n-1 threads passam para a segunda fase do algoritmo de desempate. Nesta segunda fase, o mesmo cdigo enquanto (vez[t] 0 e (vez[t], t) > (vez[j], j) executado, e pelo menos um dos n-1 threads fica preso no loop, resultando em no mximo n-2 threads passando da fase 2 para a fase 3. O mesmo raciocnio se aplica nas outras fases, e temos que na fase n-1 no mximo (n (n-1) = 1 thread consegue sair do trecho de desempate e entrar na regio crtica. De uma forma geral, as idias das solues para o problema da excluso mtua no fazem parte do arsenal dirio de um desenvolvedor de programas concorrentes, j que este se utiliza das primitivas j disponveis em seu ambiente a fim de proteger recursos compartilhados (regies crticas) do acesso concorrente. Mas o entendimento de como e porque realmente os algoritmos funcionam bem til na formao do desenvolvedor, pois os argumentos exercitam as situaes mais comuns de erros na programao concorrente: uma troca de contexto em um momento crucial, ou uma hiptese sobre o estado de uma varivel compartilhada
7
que no se mantm vlida durante toda a execuo. Outra razo para no ignorarmos estes algoritmos que em alguns casos uma forma mais relaxada (e particular para a aplicao) de excluso mtua necessria, e dificilmente tal soluo ad hoc estar disponvel no ambiente de programao. Solues particulares podem ser obtidas com pequenas alteraes nos algoritmos clssicos.
2.3. Semforos
O conceito de semforos apareceu em 1968, no trabalho de Dijkstra, como um mecanismo para sincronizao de atividades concorrentes. Um semforo um tipo de varivel, ou seja, denota no s como uma determinada informao armazenada, mas tambm como esta informao pode ser manipulada, ou seja, que operaes esto disponveis para uso da varivel. Ao invs de termos, como nas solues para o problema de excluso mtua j descritos, uma varivel normal sendo usada de forma complexa para expressar sincronismo, teramos disponvel um tipo especfico de varivel til apenas para lidar com sincronizao. As operaes disponveis para este tipo encapsulariam os detalhes complexos de como o sincronismo obtido, tornando o projeto de programas mais fcil de ser entendido e de ser verificado como correto. O conceito de semforo foi uma das primeiras (e at hoje mais importantes) ferramentas para desenvolvimento disciplinado de programas concorrentes. Outro aspecto importante dos semforos que sua forma de obter sincronizao no se baseia em espera ocupada (como o caso das solues nas sees 2.2), e sim em permitir que o thread seja posto de lado, parando de consumir recursos, at que chegue o momento dele sincronizar. O tipo semforo em geral representado nas implementaes atravs um nmero inteiro e de uma fila que descreve quais threads esto aguardando para sincronizar. O conceito de semforo (e obviamete o prprio termo) surgiu da utilizao de semforos no mundo real a fim de evitar colises de trens. Um semforo na linha ferroviria um sinalizador que indica se o trilho a frente est disponvel ou ocupado por um outro trem. Nos semforos de nossas cidades a idia semelhante: h um indicador de que a parte do cruzamento est disponvel para o usurio ou ocupada por ter sido cedida a usurios do cruzamento que vm de outra direo. Tanto semforos de linhas ferrovirias quanto de caminhos virios tem como objetivo coordenar o acesso a um recurso compartilhando, permitindo tambm sincronizao condicional (um trem fica parado at que a condio de linha ocupada mude). O tipo semforo oferece os seguintes usos: Um semforo pode ser criado recebendo um valor inicial, que indica o nmero mximo de threads que o semforo deve deixar passar em direo ao recurso
compartilhado. Por exemplo, quando inicializado com o valor 1 o semforo pode ser utilizado para prover acesso exclusivo a algum recurso compartilhado; Um semforo pode ser utilizado atravs de sua operao wait, com o seguinte significado: se o valor do inteiro armazenado no semforo for maior que zero, ento este valor decrementado e o thread que utilizou a operao pode prosseguir normalmente em sua execuo. Se o valor zero, ento o thread que chamou a operao suspenso e a informao de que este thread est bloqueado neste semforo armazenada na estrutura de dados do prprio semforo. Um semforo pode ser utilizado atravs da operao signal, que tem a seguinte funcionalidade: se no h thread algum esperando no semforo, ento incrementa o valor do inteiro armazenado no semforo. Se h algum thread bloqueado, ento libera um thread, que ter sua execuo continuando na instruo seguinte ao wait que ocasionou a espera. uma questo de implementao se o thread liberado o primeiro da fila ou no.
No trabalho original de Dijkstra, a operao wait era chamada de P, e a operao signal de V. O uso de semforos para proteger uma regio crtica de cdigo pode ento ser feita atravs do uso da seguinte varivel acessvel a todos os threads competidores: Semforo lock = new Semforo(1); // cria varivel s do tipo Semforo, // inicializando o semforo com 1 Cada thread usaria o semforo da seguinte forma: <comandos quaisquer que no faam uso de recursos compartilhados>; lock.wait(); // o thread chama a operao wait do semforo lock, ou // seja, dependendo do inteiro armazenado em lock (se este // 0 ou no), o thread pode bloquear. executa_regio_crtica(); // se a execuo chegou neste ponto, o semforo
// indicava que a regio crtica estava liberada lock.signal(); // libera a regio crtica. Se h outros threads bloqueados // aguardando no semforo, um deles volta a executar. <comandos quaisquer que no faam uso de recursos compartilhados>;
A escolha do nome lock para o semforo no foi por acaso. muito comum se referir s variveis que protegem as regies crticas como locks, j que funcionam como cadeados protetores que impedem a entrada indesejada. Em alguns ambientes, suporte para excluso mtua oferecido atravs de rotinas lock() e unlock(), em que se obtem permisso de acesso e depois libera-se o acesso para outro thread. Locks so um caso particular de semforos, pois referem-se a semforos inicializados com o valor 1. A implementao dos semforos (isto , das operaes wait e signal) deve ser feita de forma que a execuo de uma dessas operaes (onde se consulta e altera o valor de um inteiro, e s vezes se manipula a fila de processos bloqueados) seja atmica, j que um nmero arbitrrio de operaes podem ser requisitadas no mesmo semforo concorrentemente. Ou seja, a prpria implementao dos semforos (que servem para proteger regies crticas) envolve lidar com regies crticas, por exemplo atravs das solues vistas na seo 2.2. Textos tradicionais em sistemas operacionais contm muitos exemplos da soluo de problemas com o uso de semforos[Tanenbaum 1992, Silberschatz-Peterson 1988]. Como os ambientes em que desenvolveremos nossos exemplos no incluem diretamente suporte para semforos, no nos aprofundaremos no assunto.
2.4. Monitores
O uso de semforos permite diferenciar o que est sendo usado para controlar a concorrncia, mas como semforos so globais a todas os threads que queiram utiliz-los, seu uso a fim de proteger uma nica estrutura de dados, por exemplo, pode estar disperso por todo o programa. Para entender como a estrutura acessada ou verificar que ela est corretamente protegida de acesso concorrente, tem-se que vasculhar o cdigo de todos os threads. Alm disso, no h associao sinttica entre semforos e os cdigos que estes protegem, portanto nada impede que o programador utilize vrios semforos para proteger a mesma estrutura de dados, tornando difcil entender que todos os vrios trechos do programa so relacionados. Analogamente, vrias regies crticas totalmente independentes podem ter sido protegidas com o mesmo semforo, potencialmente diminuindo desnecessariamente o potencial para concorrncia. Monitores, inicialmente propostos por Hoare [Hoare, 1974] e Hansen [Hansen 1973a], so mdulos de programas que tm a estrutura de um tipo abstrato de dados. Estes mdulos contm dados encapsulados (isto , disponveis apenas dentro da implementao do mdulo) que so manipulados pelas operaes definidas no mdulo. H excluso mtua entre as operaes, isto , em nenhum momento duas operaes podero estar sendo executadas ao mesmo tempo. A implementao dos monitores deve assegurar a excluso mtua de execuo de operaes. Um compilador poderia implementar monitores associando a cada monitor um semforo; a chamada de
uma operao implicaria em um wait() e o trmino de execuo da operao um signal(). Mas, como vimos desde o incio desta seo, excluso mtua no suficiente para programao concorrente, pois para muitos problemas a sincronizao condicional essencial para que os threads colaborem. Para este fim, os monitores proveem um novo tipo de varivel: varivel de condio. Estas variveis podem ser usadas pelo programador para expressar condies de interesse para os threads. Se um thread quer, por exemplo, esperar at que uma determinada varivel assuma o valor 0, ele poderia ficar continuamente monitorando o valor da varivel at que sua condio desejada seja verdadeira, mas este enfoque tem as desvantagens usuais de uma espera ocupada. Se o thread tem disponvel uma varivel de condio cond, o thread pode bloquear a si mesmo executando a operao cond.wait(), e permanecer assim bloqueado at que algum outro Thread execute cond.signal(). Assim, para esperar at que uma determinada varivel assuma o valor 0, pode ser criada uma varivel de condio, por exemplo, espera_zero, e o thread comea a esperar executando espera_zero.wait(). Quando outros threads alterarem o valor da varivel, eles devem colaborar com o thread bloqueado executando espera_zero.signal(), que acordar o thread bloqueado, que pode ento ver se a varivel ficou com 0 e at continuar esperando novamente se for o caso. Observe que mesmo este exemplo simples j mostra como o uso de condies suscetvel a erros: se algum thread falha em fazer a sua parte esquecendo de acionar signal(), algum outro thread pode ficar bloqueado indefinidamente. As operaes wait/signal apresentam alguma semalhana com o que foi apresentado na seo 2.1, mas como as operaes s podem ser utilizadas de dentro de um monitor, sabemos que no seria possvel termos vrios wait/signal ocorrendo simultaneamente, e portanto a implementao destas primitivas fica simplificada. Mas ainda possvel ter a situao em que um signal() executado sem haver um correspondente wait() esperando-o, e neste caso o signal() no tem efeito algum. Na seo 2.1 discutimos esta situao no contexto de um thread Procura, responsvel pela tarefa de, usando algoritmos matemticos, encontrar primos, e o thread Informa, que era responsvel pelo envio de e-mails quando novos primos eram encontrados, apontando as falhas na seguinte soluo:
Thread Procura while ( ) { // executa para sempre // busca prximo primo acha_prximo_primo(); // avisa ao outro Thread SIGNAL (Informa); } }
Utilizando monitores, o programa pode ser estruturado de forma a conter os dois threads que realizam o trabalho, mas evidenciado que a colaborao entre os dois threads deve ser sincronizada pelo encontro de novos primos. Uma forma possvel de estruturar este programa seria termos um monitor e dois threads, como descrito a seguir: Monitor Primos { varivel-de-condio primo_encontrado; operao procura_prximo() { acha_prximo_primo(); primo_encontrado.signal(); } operao informa_prximo() { primo_encontrado.wait(); envia_email_para_chefe(); } } // fim do monitor Thread Procura { while (true) { Primos.procura_prximo(); } } Thread Informa { while (true) { Primos.informa_prximo(); } } Com esta soluo, nenhuma notificao deixa de ser enviada, mas observe que o potencial de concorrncia tambm foi diminudo: mesmo com dois processadores disponveis, no seria possvel que o trabalho de envio de e-mail fosse feito ao
mesmo tempo que a busca por primos. Em geral, solues com monitores colocam dentro do monitor apenas a condio de sincronizao, e no o acesso aos recursos, a fim de permitir que controlemos acesso concorrente aos recursos compartilhados. A implementao de variveis de condio tem um problema potencial. Suponha que o exemplo acima tivesse sido programado da seguinte forma: Monitor PrimosComLoop { varivel-de-condio primo_encontrado; operao procura() { while () { acha_prximo_primo(); primo_encontrado.signal(); } } operao informa() { while (){ primo_encontrado.wait(); envia_email_para_chefe(); } } } // fim do monitor O problema est na operao procura: aps executar o signal, a operao ainda est ativa, com mais coisas a serem executadas, e a operao informa tambm estaria ativa por ter sido desbloqueada pelo signal. Mas, por definio, apenas uma operao do monitor pode estar ativa a cada momento ! Uma soluo obrigar o signal a ser a ltima operao da rotina. Na seo 3 discutiremos como o modelo de concorrncia de Java e o do Pthreads abordam a questo.
As duas formas mais populares hoje em dia de programao com threads so o uso da linguagem Java (que uma linguagem de programao concorrente!) e o uso uma biblioteca de threads padro, a pthreads. Pthreads oferecem ao programador primitivas para administrao de threads (criao, cancelamento, ajuste de prioridade, etc), para acesso exclusivo a regies crticas (mutexes) e variveis de condio [Bacon 1998]. A definio completa pode ser encontrada no padro IEEE POSIX 1003.4a, e exemplo de implementao so o Solaris Pthreads e DECthreads (Multi-threading Run-time Library para OSF/1, da Digital). Uma viso geral do uso de bibliotecas de threads fornecida em [Kleiman 1995, Norton 1996, Butenhof 1997]. Como nossa inteno criar programas concorrentes para aplicaes acessveis via internet, apresentaremos aqui o modelo de threads da linguagem Java. A linguagem Java, alm de definir uma sintaxe e semntica para especificao de programas, oferece uma boa gama de pacotes prontos, isto , implementa diversos tipos abstratos de dados muito teis no desenvolvimento de programas.A unidade fundamental de programao em Java a classe [Arnold-Gosling 1996]. Classes fornecem a estrutura para os objetos (as variveis interessantes do seu programa, isto , que no sejam de tipos primitivos como inteiro, real, etc.), a forma de criar objetos a partir das definies das classes e a lista de mtodos (operaes) disponveis para manipular os objetos da classe. Do ponto de vista do programador, a essncia do suporte para concorrncia da linguagem Java oferecida em duas classes: a classe java.lang.Thread e java.lang.Runnable. Estas classes permitem que definamos threads para as nossas aplicaes e gerenciemos seus estados de execuo e a colaborao entre os threads. A classe java.lang.Runnable na verdade uma interface, isto , ela declara um tipo que consiste apenas de mtodos abstratos (no implementados) e constantes, permitindo que o tipo possa receber diferentes implementaes. A interface java.lang.Runnable contm apenas um mtodo abstrato, o mtodo run(), que representa o cdigo a ser executado pelo thread. Qualquer classe que queira implementar a interface Runnable deve fornecer o cdigo para run(). Ao criarmos um novo thread a partir de uma classe que implementa a interface Runnable, quando o thread comear a trabalhar, ser este mtodo run que ser acionado. A classe java.lang.Thread representa um thread de execuo, e oferece mtodos que permitem executar ou interromper um thread, agrupar threads, e especificar a prioridade de execuo do thread (isto , que preferncia se tem por este thread na hora de escolher um prximo thread para ocupar a CPU).
public class UsaNossoThread { public static void main ( String[] args) { // cria trs threads, fornecendo nomes escolhidos para cada um new NossoThread(Um).start(); new NossoThread(Dois).start(); new NossoThread(Trs).start(); } Este mesmo exemplo, utilizando a interface Runnable, seria definido da seguinte forma: class NossoDizOi implements Runnable { public void run() { System.out.println(Executando thread + Thread.currentThread().getName() + - Oi!); } } public class UsaNossoThread { public static void main(String[] args) { // primeiro, criamos os trs objetos que contm o cdigo a // ser rodado NossoDizOi a = new NossoDizOi(); NossoDizOi b = new NossoDizOi(); NossoDizOi c = new NossoDizOi(); // agora criamos threads, fornecendo no construtor alm do // nome que queremos, o objeto que tem o que cdigo a ser rodado. new Thread(a, Um).start(); new Thread(b, Dois).start(); new Thread(c, Trs).start(); } } Observe que o cdigo que faz o trabalho do thread mudou um pouco: na verso a partir da classe Thread, para obter o nome do thread que iria imprimir, bastava chamar getNome(), j que o objeto que chama a funo herdou a operao de Thread. J no exemplo com Runnable, primeiro temos que descobrir qual o thread que est executando run (atravs da rotina Thread.currentThread()), e a chamar getNome() para este objeto thread. H boas razes tanto para escolhermos definir threads a partir de Thread quanto a partir de Runnable. H uma regra que tem se mostrado til na prtica: Se a classe que voc est criando como cdigo do thread precisa ser derivada de alguma
outra classe, ento utilize Runnable8. Para programas que devem ser executados a partir de browsers, precisaremos que o cdigo seja subclasse de Applet, e portanto Runnable o caminho mais apropriado a ser seguido. Este mesmo exemplo, na forma de um applet a ser includo em uma pgina html, ficaria assim: // preparo para usar applets e elementos grficos import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class AppletUsaNossoThread extends Applet implements Runnable { // Estamos definindo classe de objetos que pode ser includa em pginas // html, e tambm especifica cdigo a ser executado por thread. Label label_saida = new Label("Sada: "); // elemento grfico TextArea texto_saida = new TextArea("", 10, 50); // rea para escrever Button botao = new Button("Executar"); // boto para requisitar cria// o e execuo de threads public void run() { // cdigo a ser executado nos threads // escreve na rea grfica criada para este fim texto_saida.append("Executando thread " + Thread.currentThread().getName() + "- Oi!\n"); } public void init() { // coloca itens grficos add(label_saida); add(texto_saida); add(botao); // Define, instancia e registra objeto que trata // do botao quando pressionadobotao. addActionListener( new ActionListener () { public void actionPerformed(ActionEvent e) { cria_threads(); // rotina definida a seguir }}); }
Isto est relacionado ao fato de no haver herana mltipla em Java, ou seja, no seria possvel uma classe ser subclasse ao mesmo tempo de Thread e alguma outra classe.
void cria_threads() { // agora criamos threads, fornecendo no construtor alm do // nome que queremos, o objeto que tem o que cdigo a ser rodado. new Thread(this, "Um").start(); new Thread(this, "Dois").start(); new Thread(this, "Trs").start(); } } Este applet visto pelo appletviewer tem a seguinte apresentao:
Alm da execuo exemplificada na figura acima, este programa poderia resultar ainda em mais cinco possveis sadas (duas delas exemplificadas a seguir) na rea de texto (TextArea texto_saida), dependendo de qual thread a mquina virtual de java escolheu rodar primeiro Executando thread Dois - Oi! Executando thread Trs - Oi! Executando thread Um - Oi! Executando thread Trs - Oi! Executando thread Um - Oi! Executando thread Dois - Oi!
thread est associada uma prioridade (numrica), e o escalonador escolhe threads com base nas prioridades relativas entre os threads aguardando o uso da CPU. Quando um thread criado, ele tem a mesma prioridade do thread que o criou9.As prioridades podem ser alteradas a qualquer momento atravs do mtodo setPriority. Prioridades so inteiros no intervalo definido pelas constantes disponibilizadas em java.lang.Thread: MIN_PRIORITY e MAX_PRIORITY. Um thread de maior prioridade s cede seu lugar para um thread de menor prioridade se ele terminar sua execuo, ceder espontaneamente sua vez (atravs do mtodo yield) ou no puder prosseguir sua execuo por estar aguardando algum evento. Threads de mesma prioridade so alternados, formando uma fila em que o thread, aps ser servido, vai para o final da fila. Um thread escolhido continuar executando at que uma das seguintes condies se torne verdadeira: Algum thread de maior prioridade fica pronto para executar (por ter sido criado ou por no precisar mais esperar por algo). O thread de menor prioridade que estava executando interrompido em favor do novo thread, de maior prioridade; Seu mtodo run() termina, ou ele cede a vez atravs do yield(); O ambiente de execuo permite que seja implementada a poltica de fatia de execuo, isto , entre threads de mesma prioridade cada um recebe um fatia de tempo, e sai da CPU quando sua fatia terminou. Este o caso, por exemplo, do Windows 95/NT.
Mas no h garantia de que sempre o thread de maior prioridade esteja rodando, pois o escalonador pode decidir escolher um thread e menor prioridade a fim de evitar que este espere para sempre [Tutorial Java]. O uso de prioridades til para incentivar que um dos threads consiga continuar progredindo em sua execuo. Por exemplo, se sua aplicao executa uma simulao e mostra uma visualizao da mesma, importante que o thread de visualizao tenha chance de executar frequentemente, de forma que a visualizao esteja atualizada. Por exemplo, para simular o movimento (aleatrio) de n formigas em uma rea retangular podemos usar um thread para cada formiga, com cada um desses threads mantendo disponvel a posio atual da formiga a que corresponde. Um thread extra, para visualizao da simulao, consultaria a posio corrente de cada formiga e com esta informao alteraria o display da rea retangular.
9
Observe que a execuo sempre comea com um thread, que no criado de forma explcita pela aplicao. No caso de uma aplicao a ser executada da linha de comando, um thread automaticamente criado para executar o mtodo public static void main(String[] args).
Simplesmente atribuir a este thread de visualizao uma prioridade maior do que as das prioridades dos threads das formigas no leva a uma soluo correta, pois este thread teria lugar cativo em um processador, em detrimento dos threads das formigas (ou seja, s seria uma soluo aceitvel se o nmero de processadores for pelo menos n+ 1). Usualmente o tratamento dado a este thread envolve ganhar uma prioridade maior, mas tambm deliberadamente desocupar a CPU periodicamente, a fim de que o cdigo de simulao das formigas possa executar. A operao yield(), disponvel na classe Thread, s tem efeito para ceder a vez a outro thread de mesma prioridade. A fim de passar a CPU para um thread de menor prioridade, o thread de maior prioridade pode usar o mtodo sleep(), que faz com que este pare de executar por um intervalo de tempo. H duas formas disponveis, uma em que a unidade de tempo milisegundos e a outra em que se pode tambm especificar o nmero de nanosegundos: static void sleep(long millis) e static void sleep (long millis, int nanos)10. Esta parada temporria de execuo pode ser interrompida pelo mtodo interrupt(): se o thread t1 chamou sleep, outro thread t2 que esteja executando pode chamar t1.interrupt() e com isso interromper a dormida de t1. Quando isto ocorre, t1 retorna de sleep atravs do lanamento de uma interrupo; assim, necessrio que as chamadas de sleep lidem com a possibilidade de ser lanada a interrupo InterruptedException (vide o uso de sleep nos exemplos a seguir). necessrio cuidado tambm para evitar que cada um dos threads relativos as formigas no atuem de forma egosta. Se todos so criados com a mesma prioridade, nos ambientes em que o suporte do sistema operacional para threads no envolva a alocao de fatias limitadas de tempo a cada thread, seria possvel que uma formiga monopolizasse a CPU enquanto o thread de visualizao est sem executar (dormindo por iniciativa prpria). Isto pode ser evitado se cada um dos threads, aps executar uma parte de sua simulao, cooperar com os outros atravs da chamada yield(), que faz com que a CPU passe para outro thread de mesma prioridade.
O modificador static significa que a rotina pode ser invocada sem a especificao de um objeto especfico.
10
A maior parte das aplicaes de interesse exige que threads compartilhem informao e considerem o estado e as atividades sendo exercidas por outros threads antes de progredir com sua prpria execuo. Java permite a definio de regies crticas (seo 2.2) atravs da palavra-chave synchronized. Regies crticas, em que um mesmo objeto acessado por threads concorrentes, podem ser mtodos ou blocos de comandos. A implementao de Java associa com todo objeto (estejamos ou no usando threads) um lock, isto , uma varivel para excluso mtua (como um semforo inicializado com o valor 1, conforme discutido na seo 2.3). Introduziremos o uso de threads sincronizados em Java com um exemplo onipresente: produtor/consumidor11. A aplicao formada por um thread que periodicamente (com intervalos aleatrios) produz nmeros e dois threads que consomem nmeros produzidos sempre calculando a mdia dos nmeros j consumidos. O produtor e os consumidores precisam compartilhar um repositrio de nmeros produzidos a serem consumidos. O seguinte cdigo descreve a funcionalidade do produtor, consumidor e do repositrio, mas no leva em considerao corretamente os aspectos de concorrncia: public class ProdutorConsumidor { // classe com programa principal private Produtor prod; // implementa cdigo do thread para produtor private Consumidor cons1, cons2; // implementa cdigo do thread consumidor private Repositorio reposit; // guarda nmeros a serem consumidor // construtor recebe quantidade de elementos a serem prod/cons ProdutorConsumidor(int num_elementos) { reposit = new Repositorio(num_elementos); prod = new Produtor(num_elementos, reposit); int num_cada_consumidor = num_elementos/2; cons1 = new Consumidor(num_cada_consumidor, reposit); if (num_elementos %2 != 0) num_cada_consumidor++; cons2 = new Consumidor(num_cada_consumidor, reposit); new Thread(prod, "Produtor").start(); // cria thread produtor new Thread(cons1, "Consumidor 1").start(); // cria thread consumidor new Thread(cons2, "Consumidor 2").start(); // cria thread consumidor }
11
Na aula do mini-curso uma verso mais grfica e interessante deste exemplo utilizada. Aqui, por brevidade, nos atemos ao aspecto algoritmo do problema. Todos os exemplos utilizados na aula se encontram no stio deste texto (http://www.ime.usp.br/~dilma/jai99).
public static void main(String[] args) { // Programa principal: s cria objeto ProdutorConsumidor, j que // o construtor desta classe cria os threads e comea execuo. // Argumento do construtor 10 a quantidade a ser prod/cons ProdutorConsumidor prog = new ProdutorConsumidor(10); } } // class Produtor: thread que gera nmero aleatrio, adiciona-o no repositrio // compartilhado e fica um perodo aleatrio sem executar. class Produtor implements Runnable { private Repositorio reposit; private int num_elementos; // construtor do Produtor recebe quem o repositrio e quantos elementos // devem ser produzidos. Produtor(int num, Repositorio reposit) { num_elementos = num; this.reposit = reposit; } public void run() { for (int i=0; i < num_elementos; i++) { int numero =(int) (Math.random() * 1000); // gera nmero reposit.poe(numero); // coloca na estrutura de dados // compartilhada System.out.println("Produtor colocou " + numero); // fica um tempinho sem fazer nada try { // dorme Thread.currentThread().sleep ((long) (Math.random() * 100)); } catch (InterruptedException e) { /* ignora */ } } } } // Consumidor class Consumidor implements Runnable { private Repositorio reposit; private int soma = 0; // mantm mdia dos elementos consumidos private int num_elementos; Consumidor(int num, Repositorio reposit) { num_elementos = num; this.reposit = reposit; }
public void run() { int i = 0; while (i < num_elementos) { // tem que consumir quantidade estipulada if (!reposit.vazio()) { // repositrio tem algo a ser consumido int numero = reposit.tira(); soma += numero; i++; System.out.println(Thread.currentThread().getName() + tirou " + numero + .Media + (double) soma / i); } } } } // Estrutura de dados que armazena nmeros a serem consumidos class Repositorio { int num_elementos; // tamanho da estrutura int in = 0, out = 0; // indicadores da prxima posio onde sero // colocados/retirados elementos int [ ] vetor; // local de armazenamento Repositorio(int num) { num_elementos = num; vetor = new int[num]; } void poe (int num) { vetor[in++] = num; } boolean vazio () { if (in == out) return true; else return false; } int tira () { return(vetor[out++]); } } Um dos problemas com este cdigo , obviamente, a falta de cuidado com o acesso concorrente ao recurso compartilhado (repositrio de nmeros). O thread
consumidor tenta garantir que existam elementos no repositrio antes de invocar a retirada, mas notem que o seguinte escalonamento possvel: Programa principal cria prod,cons1,cons2 insere 1 nmero vazia?no! vazia?no! retira 1 nmero tenta retirar:ERRO Seria possvel ter os dois consumidores retirando o mesmo elemento ao mesmo tempo (e portanto um elemento restaria no final sem ser consumido). Se tivssemos vrios produtores, poderia ocorrer deles tentarem colocar um novo elemento exatamente na mesma posio do vetor de armazenamento, perdendo-se um dos nmeros. necessrio ento delimitar que partes do acesso ao repositrio deve ser prevenida de acesso concorrente. A fim de lidar evitar que duas remoes (chamada reposit.tira()) interfiram uma com a outra, este mtodo deve ser executado por no mximo um thread. Se tambm queremos permitir a possibilidade de termos vrios produtores, poe() tambm deve ser protegido contra acesso concorrente. Quando ao mtodo que checa se o repositrio est vazio, tambm devemos evitar o seguinte cenrio: Thread cons1 /*chama vazia() no caso em que h um elemento: */ in==out ? NO!! /* chama vazia(), ainda h um elemento*/ in == out ? NO ret false out++ e retorna elemento ret false out++ e retorna elemento: ERRO ! Thread cons2 prod cons1 cons2
Cenrios como este indicam que a operao vazia deve ser executada com excluso mtua: enquanto ela executa nenhum outro thread pode estar executando algum mtodos do objeto reposit.. Da mesma forma, devem ser mutuamente exclusivas as execues de retira e pe. A fim de definir que a estrutura de dados s pode ser manipulada por um de seus mtodos de cada vez, basta declarar cada um dos mtodos com o modificador synchronized: class Repositorio { synchronized void poe (int num) { } synchronized boolean vazia () {} synchronized int tira() {} } Como todos os mtodos da classe foram declarados como sincronizados, a classe funciona como um Monitor (seo 2.4). O objeto reposit possui um lock interno, isto , uma varivel que indica se o objeto compartilhado est livre ou ocupado. A execuo de um mtodo synchronized (por exemplo, reposit.tira() invocada pelo thread cons1) segue o seguinte protocolo: se o lock de reposit est disponvel, o lock passa a ficar de posse do thread cons1, e cons1 prossegue executando tira(). Ao final da execuo, cons1 libera o lock. Se, por outro lado, o lock estiver ocupado (de posse de algum outro thread, por exemplo cons2), ento cons1 fica bloqueado, aguardando a liberao do lock. A obteno e liberao de um lock so feitas automaticamente pelo sistema de execuo da linguagem Java, e de forma atmica (sem interrupo). A implementao suporta sincronizao reentrante, isto , um thread requisitar um lock que no est disponvel por estar exatamente com ele: public class Reentrante { public synchronized void a () { b(); // chama b, que tambm synchronized System.out.println(em a()); } public synchronized void b() { System.out.println(em b()); } } A chamada new Reentrante().a() resulta na impresso de: em a() em b()
Voltando ao problema do produtor/consumidor, ser que agora que tornamos o acesso ao repositrio disponvel a no mximo um thread por vez, corrigimos o problema de sincronizao? Vejamos o cdigo do Consumidor: class Consumidor implements Runnable { private Repositorio reposit; private int soma = 0, num_elementos; Consumidor(int num, Repositorio reposit) { . . . } public void run() { int i = 0; while (i < num_elementos) { // tem que consumir quantidade estipulada if (!reposit.vazio()) { // repositrio tem algo a ser consumido int numero = reposit.tira(); soma += numero; i++; System.out.println(Thread.currentThread().getName() + tirou " + numero + .Media + (double) soma / i); } } } } O trecho destacado permite o seguinte cenrio se o repositrio contem apenas um inteiro: Thread cons1 if (!reposit.vazio ( ) ) /* vazio( ) retornou falso, j que h um elemento */ if (!reposit.vazio( ) ) /* vazio( ) retornou falso, j int num = reposit.tira ( ) int num = reposit.tira ( ); ERRO !! Thread cons2
desejvel que as operaes vazia ( ) e tira ( ) sejam executadas atomicamente, ou seja, uma vez constatado que h elemento disponvel no repositrio, o mesmo seja retirado sem que haja chance de interveno por parte de algum outro thread. Incluremos o mtodo seDerTira ( ) na classe Repositorio, de forma a permitir que o mtodo infome a disponibilidade e j entregue um valor se for o caso: synchronized Integer seDeTira ( ) {
if (vazio ( ) ) return null; else return new Integer ( vetor[out++] ); } Os consumidores usam este novo acesso ao repositrio da seguinte forma: class Consumidor implements Runnable { <...> public void run() { int i = 0; Integer valor ; while (i < num_elementos) { // tem que consumir quantidade estipulada if ( ( valor = seDerTira ( ) ) != null ) ) { // repositrio forneceu um valor soma += valor.intValue(); i++; System.out.println(Thread.currentThread().getName() + tirou " + numero + .Media + (double) soma / i); } } } } Vale a pena observar que se o ambiente em que executarmos o programa no implementa fatias de tempo para cada um dos threads, o seguinte poderia ocorrer: aps os trs threads (de mesma prioridade) terem sido criados, o escalonador escolhe cons1 para executar; cons1 chama vazia() e verifica que no h elemento a ser consumido, volta para o incio do loop e verifica novamente que no h elemento. Como cons1 no sai da CPU, prod nunca conseguiria executar. Isto pode ser evitado se o thread consumidor, ao notar que o repositrio est vazio, ceder sua vez para outro thread (trecho destacado com o retngulo): while (i < num_elementos) { // tem que consumir quantidade estipulada if (!reposit.vazio()) { // repositrio tem algo a ser consumido int numero = reposit.tira(); soma += numero; i++; System.out.println(Thread.currentThread().getName() + tirou " + numero + .Media + (double) soma / I);
} else Thread.currentThread().yield(); } } Outra alternativa seria dar mais prioridade ao thread prod. Alm de usar o modificador synchronized nas definies de mtodos, possvel proteger trechos de programa, isto , garantir que um thread s executa o trecho se estiver de posse de um lock especfico. Sintaticamente, fica: Object obj = ; // algum objeto synchronized (obj) { <lista de comandos> } Se o lock associado ao objeto obj est disponvel (isto , nenhum thread o tem, ou o prprio thread em execuo o tem), <lista de comandos> executado; seno o processo fica bloqueado at que o lock esteja disponvel.
Estas primitivas de sincronizao seriam teis no problema produtor-consumidor abordado na seo anterior (3.3), pois permitiriam que os threads consumidores, ao invs de executarem uma espera ocupada (como definido na seo 2.2.1) em que seguidamente testam a existncia de elementos a serem consumidos, esperassem ser notificados da produo de elementos. Em outras palavras, toda vez que o repositrio compartilhado ganhasse algum novo elemento, este notificaria o thread que desejasse executar uma remoo. O mtodo wait na verdade ainda mais poderoso, por permitir que o tempo de espera seja limitado: wait(long timeout) aguarda por notificao ou at que o perodo timeout (especificado em milisegundos) tenha passado. Est disponvel tambm wait(long timeout, int nanos), permitindo especificar com granularidade de nanosegundos. A seguir apresentamos o ProdutorConsumidor utilizando sincronizao via wait/notify, de forma que a CPU no fica indevidamente ocupada por consumidores na espera de valores. A grande diferena est no repositrio, que no mais necessida do mtodo seDerTira ( ), j que o usurio pode simplesmente requisitar um elemento (mtodo tira ( ) ), ficando bloqueado at que seu pedido possa ser atendido: class Repositorio { <...> synchronized void poe ( in t num ) { vetor [ in++ ] = num; notify ( ); } synchronized boolean vazio ( ) { if ( in == out ) return true; else return false; } synchronized int tira ( ) { if ( vazio ( ) ) { wait ( ) ; return vetor [ out++]; } } Outra primitiva de sincronizao condicional disponvel em Java (e em bibliotecas como POSIX pthreads) o join: quando um thread t1 invoca t2.join(), t1 fica bloqueado at que a execuo de t2 termine. Se t2 foi interrompido, t1 receberia
esta informao via uma exceo. A espera pode, como no caso do wait, ser limitada: t2.join(long timout) faz com que t1 espere por no mximo timeout milisegundos pelo trmino de t212; join(long timeout, int nanos) permite que nanosegundos sejam especificados. Um exemplo de uso do join no exemplo ProdutorConsumidor seria termos o programa principal esperando pelo trmino dos threads por ele criados (prod, cons1, cons2) a fim de coletar estatsticas finais sobre os nmeros gerados e consumidos.
3.6 Daemons
Daemons so threads que continuam executando enquanto tiver algum thread (que no seja daemon) ativo. Threads podem ser especificados como daemons atravs do mtodo setDaemon ( )., que deve ser chamado antes do mtodo start ( ) .
para suspender um thread (suspend), reativ-lo (resume) e termin-lo (stop). Estes mtodos agora no fazem parte da interface disponvel em Java 1.2. Eles foram eliminados por serem inerentemente inseguros. Por exemplo, o mtodo stop() para parar um thread fazia com que o thread liberasse todos os locks que estivessem em sua posse, podendo permitir que objetos protegidos por estes locks fossem liberados ainda estando em um estado inconsistente. Este comportamento se manifesta de forma muito sutil e difcil detectar e corrigir problemas relacionados a locks liberados antes da hora. recomendvel que programadores que desejam cessar a execuo de um thread utilizem uma varivel para indicar que o thread deveria parar de rodar (isto , sair do mtodo run() graciosamente, ao invs de sair como resultado da chamada de stop()). O thread t a ser parado deve checar regularmente esta varivel, e o thread que deseja causar o trmino de t deve modificar o valor da varivel no momento desejado. A fim de garantir uma sincronizao correta entre o thread que quer terminar t e o prprio thread t a ser terminado, importante que a varivel utilizada para esta comunicao seja declarada como volatile. Como na linguagem C, o uso deste modificador proibe que o compilador faa otimizaes de cdigo baseadas em consultar o valor da varivel atravs de registradores (o que resultaria em problema, pois o efeito de outro thread alterando a varivel no seria percebido at que o cdigo voltasse a checar a varivel e no o contedo do registrador). A declarao volatile indica que pode haver acesso concorrente a varivel no sincronizado. O seguinte cdigo, disponvel em [Tutorial Java], exemplifica esta forma de causar o trmino de threads sem utilizar o mtodo stop dentro da definio de um applet com os seguintes mtodos start, run e pra:: private volatile Thread blinker; // Thread rodando no applet que faz com // que elemento grfico fique piscando. A idia que esta varivel // guarde a referncia do Thread, e que fique nula como indicao // de que o thread deve morrer. public void start() { blinker = new Thread (this); blinker.start(); } public void run() { Thread thisThread = Thread.currentThread(); // Uma verso baseada em stop teria while (true) no corpo do run, // indicando que o mtodo roda para sempre, at que seja morto. // Aqui faremos com que ele morra graciosamente, na hora certa. while (blinker == thisThread) { // enquanto no ficou nulo // observe que blinker, por estar em um loop, poderia ter sido alocada // a um registrador, no fosse sua alocao declarada como volatile, o que
// garante que todo acesso a esta varivel causa um fetch da memria. try { // temos que usar try porque sleep pode ser interropido thisThread.sleep(intervalo); } catch (InterruptedException e) { /*ignora interrupo */} repaint(); // chama paint, que causa o efeito visual desejado } public void pra() { // usar blinker.stop() seria inseguro (alm do compilador nos avisar que // mtodo est deprecated. Utilizaremos varivel blinker. blinker = null; } Observe que a chamada ao mtodo sleep em run() indica um intervalo entre checagens. Se o intervalor for grande, bastante tempo poderia passar antes que o thread percebesse que deveria morrer. Para esses casos, o mtodo de trmino do thread (pra(), no exemplo) poderia, alm de alterar o valor da varivel de controle, interromper a dormida atravs de interrupt(). Alm de Thread.stop(), os mtodos Thread.suspend() e Thread.resume ( ) tambm foram eliminados na Java 1.2. O motivo que estes mtodos eram inerentemente passveis de deadlock (seo 2.2): se o thread a ser suspendido estava de posse de um lock correspondente a um objeto compartilhado crtico ao sistema no momento em que foi suspenso, nenhum outro thread poder acessar o recurso at que o thread fosse reativado (atravs de resume()). Se mostrou comum que o thread que iria chamar o resume() tentasse antes obter o lock do objeto compartilhado, e portanto ficava esperando por um evento (a liberao do lock) que s pode ser gerado pelo thread que espera pelo resume, vivenciando assim uma tpica situao de deadlock. Recomenda-se que o efeito de desativao temporria de threads obtido pelo suspend/resume seja implementado via wait/notify, pois com o wait() h a liberao automtica do lock, at o retorno execuo. Vejamos um exemplo: private volatile boolean threadSuspenso; public void suspende() { threadSuspenso = true; } public void desuspende() { threadSuspenso = false; } public void run() { while (true) { // suponha que o thread vai executar para sempre try { // vamos chamar sleep, e a ver se foi suspenso ou no Thread.currentThread().sleep(intervalo);
// chamaremos wait se suspenso, o que exige posse do lock synchronized (this) { while (threadSuspenso) wait(); } // fim do trecho sincronizado } catch (InterruptedException e) { } } } Esta soluo tem o inconveniente de requerer synchronized(this) toda vez que o sleep() termina sua execuo. Mtodos de sincronizao em Java so caros! Uma soluo melhor seria requerer o lock do objeto s se percebe-se que a varivel threadSuspenso est verdadeira, ou seja, s no caso em que h chance13 de querermos chamar o wait() e por isso precisamos do lock. Assim, o loop (delimitado por um retngulo) poderia ser substitudo por: if (threadSuspenso) { synchronized (this) { while (threadSuspenso) wait(); } }
Falhas de segurana (safety) levam a comportamento indesejvel do software, com funcionamento errneo. Falhas de vivacidade (liveness) levam ausncia de comportamento esperado, isto , algo pedido pelo usurio ou pelo software nunca tendo a chance de ser finalmente executado. uma prtica padro em engenharia enfatizar primeiramente o aspecto de safety do projeto, evitando que seu comportamento leve a um comportamento aleatrio e talvez perigoso. Por outro lado, a maioria do tempo empreendido no ajuste de programas concorrentes est relacionado com aspectos de eficincia e liveness, algumas vezes sacrificando Note que existe a chance de checarmos a varivel threadSuspenso, vermos que est verdadeira, mas durante o tempo que levamos para conseguir executar o synchronized(this), a varivel j ter sido alterada pela chamada de desuspende().
13
segurana em nome da eficincia ou garantia do progresso de execuo de forma bem fundamentada. O grande desafio conseguir ter os dois aspectos funcionamento bem. Duas propriedades importantes relacionadas segurana so a ausncia de deadlock e a excluso mtua no acesso a recursos compartilhados crticos. Para o caso do deadlock14, algo ruim significa ter-se threads esperando por eventos que nunca ocorrero; no caso de excluso mtua, ruim ter-se dois ou mais threads executando partes de regies crticas ao mesmo tempo. Exemplos da propriedade de liveness so a garantia de que o requisito por algum servio ser alguma hora atendido, que algum thread aguardando pela entrada na regio crtica vai alguma hora ter sucesso. Do ponto de vista de corretude, provar que propriedades de safety so atendidas significa modelar o programa concorrente em termos de aes baseadas em um estado, e provar que as aes executadas pelo programa preservam as propriedades desejadas, isto , que predicados desejveis sobre o estado do programa se mantm vlidos durante a execuo. J provar que a propriedade de liveness respeitada implica em garantir que a poltica de escalonamento (isto , da escolha das aes a serem executadas a cada momento) justa. Diz-se que uma poltica de escalonamento incondicionalmente justa se toda ao atmica que no depende de outras (isto , incondicionalmente executada) elegvel para ser executada em algum momento. J justia fraca descreve a situao em que a poltica incondicionalmente justa e tambm toda ao condicional elegvel para execuo alguma hora se sua condio se tornar verdadeira e no se tornar falsa novamente a no ser pelo efeito da prpria ao. Justia forte implica em ser incondicionalmente justa e que toda ao condicional atmica consiga executar alguma hora, assumindo que sua condio fica verdadeira um nmero infinito de vezes. Na prtica, existem estratgias conservadoras inerentemente resultam em projetos seguros: de desenvolvimento que
Imutabilidade: se evitamos mudanas de estado, evitamos que problemas de excluso mtua ocorram. Java, por exemplo, fornece meios com os quais especificar que determinadas variveis no devem nunca mudar de valor. Mas, obviamente, na maior parte das aplicaes impossvel obter a funcionalidade desejada lidando apenas com constantes. Sincronizao: se dinamicamente se garante acesso exclusivo, de forma mtodica, o problema de segurana est em parte resolvido. Por exemplo, se
14
Observe que deadlock est tambm relacionado a liveness, j que no permite que nada ocorra dali em diante.
metodicamente se usa sincronizao total (isto , todo e qualquer mtodo relacionado a um recurso compartilhado declarado como synchronized), a chance de conflitos entre acessos eliminada. Mas tambm a chance de execuo concorrente eliminada. Com sincronizao parcial, poderia-se metodicamente sincronizar somente os trechos de cdigos em que variveis mutveis so manipuladas. Mas ainda assim pode-se estar limitando em demasiado a concorrncia. Agregao: se estruturalmente garantimos que para se ter acesso a uma parte de um recurso compartilhado, necessrio ter acesso ao elemento que contm todas as partes, os problemas de excluso mtua ficam mais raros. Os objetos compartilhados so ento estruturados de forma em que um contenha o outro, e seja dono de seus objetos internos. Assim, um mtodo que tenha obtido acesso exclusivo ao recurso de fora (o que contm outros), ganha de graa acesso exclusivo aos objetos internos. Em outras palavras, este enfoque limita o compartilhamento de objetos, exigindo que o compartilhamento seja hierrquico.
Os problemas relacionados a liveness so to srios quanto os de safety, e em muitas vezes bem mais difceis de serem detectados. Threads podem falhar em termos de vivacidade devido a disputas por recursos compartilhados (em que o thread repetidamente perde a disputa), dormncia (um wait em que o correspondente notify nunca gerado) e deadlock (dois ou mais threads bloqueiam-se mutuamente enquanto tentando obter locks e assim continuar a atividade). Evitar deadlocks importante durante o projeto de programas concorrentes. Uma forma simples e muito recomendada prevenir deadlocks atravs de uma ordenao das variveis de condio (usadas em sincronizao condicional) usadas no programa, isto , exigir que um thread passe por algumas sincronizaes condicionais antes de passar por outras. Este enfoque foi amplamente estudado pela comunidade de banco de dados, gerando protocolos de controle de concorrncia como o Two Phase Locking (2PL, locking em duas fases), em que nenhum lock pode ser liberado at que todos os necessrios para a atividade tenham sido obtidos. necessrio que haja um balanceameto entre as medidas adotadas para lidar com safety e as adotadas para lidar com liveness. Em geral, um enfoque estrito para lidar com safety aumenta o potencial para problemas de liveness (alm de simplesmente diminuir a concorrncia), e um enfoque em que se tente colocar mais threads executando aumenta a chance que eles interfiram de forma indesejada, gerando problemas de safety.
Em http://www.ime.usp.br/~dilma/jai99 voc encontrar as aplicaes descritas na seo 1.3 detalhadamente documentadas, de forma que voc possa desenvolver seus objetos concorrentes com acesso remoto sem necessariamente estudar computao distribuda. Aqui no texto includo, por questo de espao, um exemplo bem mais simples: a aplicao formada por um objeto cuja funcionalidade somar um ao inteiro que ele armazena. O objeto pode ser acessado concorrentemente via applets. A implementao descrita a seguir formada pelos seguintes mdulos (arquivos):
SomaUm.html, que disponibiliza a aplicao para usurios da Teia SomaUmApplet.java, que implementa o applet que est embutido na pgina SomaUm.html SomaUm.java, SomaUmImpl.java e policy, que implementam um objeto servidor, a ser utilizado pelos clientes via applet. O acesso a este objeto, que reside na mquina sushi.ime.usp.br, feito atravs suporte fornecido na linguagem Java para chamada de mtodos em objetos remotos (RMI, remote method invocation).
Comecemos olhando o fonte SomaUm.html, a partir do qual o usurio pode usar nossa aplicao e solicitar que o inteiro seja incrementado:
<html> <head><title>SomaUm</title></head> <body> <center><h1>SomaUm</h1> </center> <applet codebase="executaveis/" code="SomaUmApplet" width=500 height=200> </applet> </body> </html> A incluso do applet est especificada no trecho delimitado pelo retngulo, indicando que o cdigo do applet est no subdiretrio executaveis, a partir do diretrio que contem o arquivo SomaUm.html. Foram tambm fornecidos o nome do cdigo a ser carregado e a rea que deve ser ocupada pelo applet na pgina sendo visualizada pelo browser. A classe SomaUmApplet implementa a funcionalidade do applet, e descrita a seguir. Atravs do applet, o usurio pode especificar o nmero de vezes em que ele deseja invocar o mtodo remoto; para cada chamada criado um thread. O applet vai mostrando o valor retornado a cada iterao, e no final o tempo total que o processo levou. O applet tem a seguinte aparncia:
O cdigo correspondente segue abaixo. import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.rmi.Naming;
// Necessrio porque este applet vai // procurar acessar um objeto em outra // mquina import java.rmi.RemoteException; // Importa definio de excees geradas // durante chamada remota de um mtodo
public class SomaUmApplet extends Applet implements Runnable{ String message = "blank"; // guarda mensagem devolvida pelo obj remoto int nb_iterations = 0; // usurio escolhe quantas chamadas devem ser // feitas. // "obj" o identificador usado para nos referirmos ao objeto remoto, isto , // ele armazena uma referncia para o objeto remoto. SomaUm obj = null; Label label = new Label("Number of Iterations: "); // elemento grfico TextField texto = new TextField("10", 12); // para entrada de num. iter. Button b = new Button("Run!"); // elemento grfico em que usurio // indica incio do trabalho do applet. public void run() { // cdigo a ser executado por um thread long delta0 = System.currentTimeMillis(); // pega hora atual e guarda. for (int i=0; i < nb_iterations; i++) { // nb_iterations foi dado pelo usurio try { // chamaremos mtodo remoto, o que pode lanar exceo. message = obj.incr(); // aqui est a chamada do mtodo. repaint(); // atualiza a tela (veja paint a seguir). } catch (Exception e) { // se exceo for lanada, trata-a System.out.println("SomaUmApplet exception in rmi call incr(): " + e.getMessage()); e.printStackTrace(); } } long total = System.currentTimeMillis() - delta0; // calcula tempo // gasto. message = "Time: " + total + "msec"; repaint(); // atualiza tela. } public void init() { // mtodo a ser executado na inicializao do
// sendo uma varivel // com valor final, podemos // utilizar dentro do cdigo a // ser defindo para o boto // Run. // coloca elementos grficos
// Define, instancia e registra um objeto (ActionListener) para lidar // com a presso do boto que colocamos na interface. b.addActionListener( new ActionListener () { public void actionPerformed(ActionEvent e) { // aqui vai o cdigo // a ser executado quando o boto pressionado. Graphics g = getGraphics(); // precisamos acionar a parte grfica g.drawString(" ", 30, 60); // limpamos campo em // msg escrita String nbstr = texto.getText(); // texto digitado pelo usurio int nb = 0; boolean gotvalue = true; try { // vamos tentar transformar string em nmero; temos que // estar preparados para lidar com textos no traduzveis. nb = Integer.parseInt(nbstr); } catch ( NumberFormatException exc) { gotvalue = false; // texto que o usurio digitou no // corresponde a nmero. g.setColor(Color.red); // Colomos msg de erro. g.drawString("Number is not valid.", 40, 70); g.setColor(Color.black); } if (gotvalue) { // conseguiu nmero thisapplet.nb_iterations = nb; // acerta valor no objeto. new Thread(thisapplet).start(); // cria thread para executar as iteraes } } }); resize(500, 200); // acerta tamanho do applet na pgina. } public void start() { // parte executada quando o applet comea a trabalhar try { // Primeiro, acha o objeto remoto, preparado para no dar certo obj = (SomaUm)Naming.lookup("//" + getCodeBase().getHost() + "/SomaUmServer"); }catch (Exception e) { System.out.println("SomaUmApplet exception in Naming.lookup: " + e.getMessage()); e.printStackTrace(); } }
public void paint(Graphics g) { // usado para atualizar a imagem do applet g.drawString(message, 25, 50); } } SomaUm.java apenas define a interface oferecida pelo objeto remoto: import java.rmi.Remote; import java.rmi.RemoteException; public interface SomaUm extends Remote { String incr() throws RemoteException; } SomaUmImpl.java efetivamente implementa o objeto remoto: // importa classes necessrias por ser objeto remoto. import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import java.rmi.server.UnicastRemoteObject; import java.io.*; // lida com entrada e sada via arquivo. import java.lang.*; public class SomaUmImpl extends UnicastRemoteObject // // Ele estende a classe UnicastRemoteObject // de forma a comunicar-se via socket e estar // executando durante todo o tempo. A alternativa // usar Remote Object Activation, mas aqui iremos // pelo jeito mais simples. implements SomaUm {// garante que mtodo incr est disponvel. int num = 1; // cria varivel num com valor inicial um. Este o valor que // ser incremetado a cada execuo do mtodo incr(). public SomaUmImpl() throws RemoteException { super();// criao simplesmente delega trabalho para superclasse. }
num++; // S para exemplificar como mexer em arquivo aqui no objeto // servidor, vamos acrescentar este nmero a um arquivo de nome // ./data. Observe que o policy definido para este cdigo estabeleceu // que este programa pode ler e escrever neste arquivo. String filename = new String("/home/mac/dilma/www/java/testes/) + String(servidorRemoto/SomaUm/data"); FileWriter out = null; try { out = new FileWriter(filename,true); } catch (IOException e) { System.out.println("Nao conseguiu abrir para escrita.\n"); } catch (SecurityException e) { System.out.println("Problema de seguranca na escrita.\n"); } // Abriu data para escrita do tipo append. String numst = new Integer(num).toString() + "\n"; try { // escrita pode gerar exceo out.write(numst,0,numst.length()); } catch (IOException e) { System.out.println("Nao conseguiu escrever o int.\n"); } try { // fechar o arquivo tambm pode dar problema. out.close(); } catch (IOException e) { System.out.println("Nao conseguiu fechar.\n"); } return "Agora tem " + num; // Valor retornado para quem ativou // a execuo deste mtodo. } public static void main(String [ ] args) { // Cria e instancia um gerente de segurana. if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); }
try { SomaUmImpl obj = new SomaUmImpl(); // Associa este objeto com o nome "SomaUmServer" Naming.rebind("//sushi.ime.usp.br/SomaUmServer", obj); // acima foi usada porta default 1099 System.out.println("SomaUmServer bound in registry"); } catch (Exception e) { System.out.println("SomaUmImpl err: " + e.getMessage()); e.printStackTrace(); } } A fim de evitar problemas de segurana, foi criado com a ferramenta policytool a seguinte classe que especifica os direitos fornecidos a SomaUmImpl: /* AUTOMATICALLY GENERATED ON Wed May 05 18:36:09 GMT03:00 1999*/ /* DO NOT EDIT */ grant { permission java.io.FilePermission "/home/mac/dilma/www/java/testes/servidorRemoto/SomaUm/data", "read, write"; permission java.net.SocketPermission "sushi.ime.usp.br", "accept, connect, listen, resolve"; };
6. Modelagem e Projeto
Embora a maioria dos programadores lide com threads concorrentes de forma ad hoc, sem apoio de mtodos ou tcnicas sistematizadas, dois enfoques principais para modelagem e projeto de sistemas concorrentes so apresentados na literatura [Nielsen-Shumate 1987, Simpson 1986, Gomaa 1993, Sanden 1997]. O primeiro baseia-se em decompor o sistema em mdulos que expressem transformaes. A decomposio expressa em um diagrama de fluxo de dados (DFD) que capture as transformaes sucessivas, se desejado em diferentes nveis de abstrao. As notaes usuais da anlise estruturada para DFDs so estendidas de forma a incluir informao de sincronizao entre transformaes. O segundo enfoque aborda a modelagem concentrando-se nos aspectos inerentemente concorrentes do problema, enfatizando o relacionameto entre o problema e a soluo ao invs da srie de passos que leva o primeiro ao segundo. Por exemplo, o mtodo ELM (Entiy-life modeling) [Sandem 1997] assume que um sistema deve reagir a eventos (ou mesmo gerar eventos) do ambiente do problema, onde cada evento, embora
no estando necessariamente associado a tempo ou durao, requer uma quantidade finita de processamento. O desenvolvedor fazendo a modelagem deve identificar quais so os eventos do sistema, e atribuir os eventos a diferentes threads. Uma forma de fazer esta atribuio criar uma linha imaginria de eventos (com vrias bifurcaes na linha, se for o caso), e particionar o rastreio de eventos representados pelas linhas entre threads, de forma que cada evento pertena a exatamente um thread e os eventos em um mesmo thread estejam suficientemente separados de forma a haver tempo para lidar processar cada um deles. Estes particionamentos de eventos em threads (threads model) sugerem a estrutura da soluo de software. Um modelo de threads considerado minimal se h um ponto na execuo em que todos os threads tm um evento. O nmero de threads em tal soluo minimal indica o nvel de concorrncia do problema. Alm dos threads, o modelo pode conter objetos compartilhados (ou no) entre os threads. A idia principal , sempre que possvel, esconder aspectos de excluso mtua (definio de regies crticas) dentro dos objetos. Recentemente, o uso de padres de projeto (design patterns) tem se mostrado til para ajudar a estruturar o projeto de programas concorrentes. Um padro descreve uma forma de projeto, usualmente uma estrutura de objetos (tambm conhecida como micro-arquitetura) consistindo de uma ou mais interfaces, classes ou objetos que obedecem a restries e relacionamentos particulares (estticos ou dinmicos) ao problema. Patterns capturam solues padres e suas variaes, levando ao reuso de componentes e arcabouos (frameworks). O livro de Doug Lea [Lea 1997] enfatiza o uso de padres como uma forma de diminuir a distncia entre teoria e prtica de programao concorrente. Segundo Lea, a pesquisa na rea de concorrncia baseia-se em modelos e tcnicas de difcil utilizao no desenvolvimento de software do dia a dia (por exemplo, uso de modelos e notaes formais em que problemas reais so de difcil derivao e manipulao incremental no ciclo de desenvolvimento). Muito da terminologia e notao adotada uma adaptao do trabalho seminal de Gamma, Helm, Johnson e Vlissides [Gang-ofFour 1994]. A utilizao de solues para problemas recorrentes, como os apresentados por Lea, um bom antdoto contra a tendncia de desenvolver programas com threads com uma mentalidade de hacker. A proliferao do uso da linguagem UML [UML 1998] e de mtodos baseados em casos de uso [Unified 1999] no desenvolvimento de sistemas orientados a objetos tambm tem potencial para melhorar aspectos de modelagem e projeto de programas concorrentes em Java, j que a notao e os passos recomendados pelo mtodo enfatizam a anlise da interao entre os objetos concorrentes que formam o modelo de objetos.
7. Bibliografia
[Arnold-Gosling 1996] K. Arnold, J. Gosling. The Java Programming Language. Addison Wesley. [Andrews 1991] G. R. Andrews. Concurrent Programming: Principles and Practice. Addison-Wesley. [Bacon 1998] J. Bacon. Concurrent Systems - Operating Systems, Database and Distributed Systems: An Integrated Approach. Addison-Wesley, Segunda Edio. [Butenhof 1997] Wesley. D. R. Butenhof. Programming with POSIX Threads. Addison-
[Eclipse 1998] B. Ozden, A. Silberschatz, J. Bruno e E. Gabber. The Eclipse Operating System: Providing Quality of Service via Reservation Domains. Proc. USENIX Annual Technical Conference, Seattle, Junho. [Farley 1998] J. Farley. Java Distributed Computing. OReilly & Associates.
[Gang-of-Four 1994] E. Gamma, R. Helm, R. Johnson e J. Vlissides. Design Patterns. Addison-Wesley. [Gomaa 1993] H. gomaa. Software Design Methods for Concurrent and RealTime Systems. Addison-Wesley Longman. [Hansen 1973] B. Hansen. Operating Systems Principles. Prentice-Hall.
[Hansen 1973a] B. Hansen. Concurrent programming concepts. ACM Computing Surveys, 5(4). [Hoare, 1974] C. A. R. Hoare. Monitors: an operating system structuring concept. Communications da ACM, 17(10). [Kleiman 1995] S. Kleiman et al. Programming with Threads. Prentice Hall.
[Lea 1997] D. Lea. Concurrent Programming in Java: Design Principles and Patterns. Addison-Wesley. [Magee-Kramer 1999] J. Magee e J. Krammer. Concurrency: State Models & Java Programs. John Wiley & Sons. [Milner 1995] R. Milner. Communication and Concurrency. Prentice Hall.
[Nielsen-Shumate 1987] K. W. Nielsen e K. Shumate. Designing Large Real-time Systems with Ada, Communications da ACM, 30(8), pg 1073. [Norton 1996] S. J. Norton et al. Thread Time: The Multithreaded Programming Guide. Prentice Hall.
[Ousterhout 1996] J. Ousterhout. Why Threads are a bad idea (for most purposes). Palestra convidada na USENIX Technical Conference, 25/jan/1996. Disponvel na WWWem http://www.scriptics.com/people/john.ousterhout em Maio/1999. [Rosu 1997] D. Rosu, M. B. Joses e M. Catalin Rosu. CPU Reservations and Time Constraints: Efficient, Predictable Scheduling of Independent Activities, Proc. of the 16th ACM Symposium on Operating Systems Principles, Frana. [Sandem 1997] Software. B. I. Sanden. Modeling Concurrent Software, IEEE
[Silberschatz-Peterson 1988] A. Silberschatz , I. Peterson. Operating System Concepts. Addison Wesley. [Simpson 1986] H. Simpson. The MASCOT Method. IEE/BCS Software Eng. Journal, 1(3), pg 102-120. [Smart 1997] J. Nieh e M. Lam. SMART UNIX SVR4 Support for Multimidia Apolications, Proc. 16th ACM Symposium on Operating Systems Principles, Frana. [Tanenbaum 1992] A. S. Tanenbaum. Modern Operating Systems.Prentice Hall. [Tutorial Java] Tutorial Java disponibilizado na WWW pela Java SunSoft. Disponvel em http://java.sun.com/docs/books/tutorial/essential/threads em Maio/1999. [UML 1998] G. Booch , J. Rumbaugh e I. Jacobson. The Unified Modeling Language User Guide. Addison-Wesley. [Unified 1999] I. Jacobson, G. Booch e J. Rumbaugh. Unified Software Development. Addison-Wesley.