Você está na página 1de 42

1

Introduo aos Processos Leves (Threads)


Joo Paulo F. W. Kitajima Marco Aurlio de Souza Mendes Departamento de Cincia da Computao Universidade Federal de Minas Gerais Caixa Postal 702 30161-970 - Belo Horizonte, MG tel: (031) 499 58 60/fax: (031) 499 58 58 e-mail: {kitajima, corelio}@dcc.ufmg.br

Introduo

A importncia do processamento paralelo na busca por maior poder de computao est atualmente bem definida. O princpio do "trabalho cooperativo" bastante intuitivo e pode ser diretamente aplicado (1) em novas arquiteturas de computadores, onde vrios processadores trabalham

simultaneamente na resoluo de um problema especfico, (2) em novos sistemas operacionais, que suportam processos concorrentes (em multiprogramao e/ou em multiprocessamento), e (3) em novas linguagens de programao, permitindo que solues sejam expressas de acordo com um paradigma de programao concorrente e/ou paralela. Nas Jornadas de Atualizao de Informtica de 1995, o primeiro autor aborda o item (3), apresentando elementos de linguagens para programao distribuda (i.e., baseada na troca de mensagens) [KIT95]. Neste texto, os autores abordam o item (2), onde alguns mecanismos do sistema operacional de suporte concorrncia so apresentados, com reflexos, naturalmente, sobre as linguagens de programao disponveis no sistema em questo. Trs exemplos so apresentados: dois sistemas operacionais multithreaded (Solaris e Eindows NT) e uma linguagem atual de programao orientada a objetos, Java, que implementa classes associadas s threads.

1. Gerncia de Processos

Computadores (isolados, paralelos ou em rede) realizam tarefas. Estas tarefas podem ser de diferentes naturezas: por exemplo, a execuo de um programa objeto compilado a partir de um

programa fonte em C, a prpria compilao deste programa, a prpria edio do programa fonte em C, controle de temperatura de uma estufa, e gerncia da memria do prprio computador e dos dispositivos de entrada e sada conectados a ele. Alm do mais, estas tarefas podem estar em execuo todas ao mesmo tempo, seja compartilhando um nico processador, e, neste caso, falamos em multiprogramao, seja utilizando vrios processadores, considerando assim a ocorrncia de multiprocessamento ou, to simplesmente, de execues em paralelo.

Sistemas Operacionais. Todo sistema computacional, exceto os mais primitivos ou os muito especializados, so dotados de um sistema operacional. Este sistema operacional possui duas funes bsicas [TAN92]:

1. apresentar ao usurio do computador (seja um programador de linguagem de alto nvel, seja um usurio de aplicativos) uma mquina estendida ou virtual. O sistema operacional funciona ento como um intermedirio entre as aplicaes e o hardware: os detalhes reais do hardware tornamse transparentes para os usurios. Se um sistema operacional com tal funo no existisse, um programador precisaria, por exemplo, saber ativar o barramento de controle e de dados de maneira apropriada a fim de realizar uma leitura de uma posio da memria principal e copiar o valor lido para outra posio da memria. Mesmo em linguagem de montagem (assembler), esta operao se resume, em geral, s seguinte instrues:

LOAD (X) STORE (Y)

% registrador recebe valor da posio X da memria % posio Y da memria recebe valor do registrador

Compiladores e linguagens de comandos (ou shells) so outros componentes (no do sistema operacional) que complementam esta funo do sistema operacional de esconder detalhes do hardware dos usurios finais dos computadores;

2. gerenciar os recursos de um sistema computacional, sejam recursos de software, sejam recursos de hardware. Por exemplo, a organizao dos arquivos em disco, o controle da multiprogramao e controle de utilizao da impressora. Sob este aspecto, o sistema operacional visto como uma entidade de controle [SIL94].

Um sistema operacional um programa ou um conjunto de programas que implementam estas duas funes acima. Dentro de cada possibilidade (fornecer uma mquina virtual e gerenciar recursos), as atividades realizadas variam de sistema operacional para sistema operacional. Podemos encontrar sistemas operacionais completos, fornecendo todos os servios imaginveis aos usurios, e sistemas operacionais menos potentes, onde alguns servios devem ser realizados pelo prprio usurio. Exemplos de sistemas operacionais so Unix (e suas inmeras variaes), DOS, Windows NT, OS/2, MACH e outros menos conhecidos, como QNX, Amoeba, Chorus, e Helios. importante observar que, por ser um ou mais programas, um sistema operacional tambm composto de um conjunto de tarefas que so executadas pelo processador. Sistema operacional software, embora algumas de suas funes possam ser implementadas em hardware. A Figura 1 apresenta uma viso modular e hierrquica de um sistema computacional.

Usurio 1

Usurio 2

Usurio 3

Usurio N

Programas de Aplicao

Sistema Operacional Hardware do Computador

Figura 1. Uma viso modular e hierrquica de um sistema computacional [SIL94].

Processos. Independente se as tarefas so de usurios tpicos, de programadores ou do sistema operacional, elas so executadas pelo processador e so gerenciadas pelo prprio sistema operacional (neste sentido, o sistema operacional se executa utilizando os seus prprios mecanismos de controle). Toda tarefa em execuo que envolve um programa objeto (cdigo), dados e um estado visto pelo sistema operacional como um processo sequencial ou, simplesmente, processo. Informalmente, um processo um programa em execuo [SIL94]. Um processo no um programa. Um programa um arquivo armazenado geralmente em um meio magntico ou ptico,

escrito em uma linguagem de alto ou baixo nvel. Um processo um programa em execuo. Uma receita de bolo um programa. Fazer o bolo usando a receita um processo. Processos so abstraes (da atividade da CPU) que so manipulveis pelo sistema operacional. Um sistema operacional enxerga uma tarefa como um processo. Eventualmente, uma tarefa de um usurio vista pelo sistema operacional como um conjunto de processos. Por exemplo, um processamento em lote (batch) envolve um job que se pode consistir em: (1) compilar um programa, (2) lig-lo a outros mdulos pr-compilados e (3) executar o programa objeto ligado final. Para o sistema operacional, este job realizado atravs de trs diferentes processos, um para cada etapa.

Antes de entrar em detalhes sobre como processos so implementados, importante observar que processos so criados por outros processos. Quando um computador ligado, um programa de boot carrega mdulos do sistema operacional que, a partir de sua execuo, lanaro outros processos do sistema e suportaro processos de usurios. Qualquer processo, por sua vez, pode lanar novos processos, formando ento uma estrutura hierrquica de processos.

Processos tambm possuem estados e podem interagir com outros processos. Alm disto, como visto acima, em um dado sistema computacional, podem existir vrios processos em execuo, normalmente, associados a diferentes usurios de um computador multiprogramado. O escalonador o mdulo do sistema operacional que decide a ordem de execuo dos processos, dado que um ou mais processadores esto disponveis. Processos podem ser preemptveis, se eles podem ser interrompidos durante a sua execuo a fim de que outro processo execute, ou processos podem ser no preemptveis, se eles no podem ser interrompidos (ou seja, executam do incio ao fim ou explicitamente se bloqueiam). Aps a interrupo de um processo ou o seu fim, o escalonador deve decidir quem deve executar em seguida. Tomada a deciso, o despachante (em ingls, dispatcher) efetivamente ativa o processo escolhido para execuo. Pelo fato de que processos podem interagir com outros processos, processos podem bloquear-se durante esta interao. Por exemplo, um processo, a fim de que possa continuar o seu trabalho, pode aguardar dados oriundos de outro processo. Enquanto espera, o processo est parado, bloqueado. Assim, possvel observar que processos podem estar em diferentes estados. Do discurso acima, trs estados so citados:

a) um processo em execuo (running): o processo cujas instrues o processador est correntemente executando;

b) um processo pronto para executar (ready): o processo que pode ser executado pelo processador, mas no est em execuo pois no foi escolhido ainda pelo escalonador; c) um processo bloqueado (blocked): como um processo pronto para executar, um processo bloqueado tambm est em espera, mas no para ser escolhido pelo escalonador. Ele espera a ocorrncia de um outro evento, por exemplo, recepo de dados, a passagem de 5 segundos ou o trmino de uma operao de entrada e sada (I/O). Quando o evento ocorre, o processo muda de estado, passando de bloqueado para pronto para executar.

A Figura 2 apresenta um diagrama de transio dos possveis estados de um processo. O modelo de processos facilita a compreenso da dinmica do computador. Uma outra viso possvel aquela baseada em interrupes: nesta abordagem, diferentes interrupes esto ocorrendo no sistema. A cada interrupo ocorrida, uma tarefa deve ser realizada. Esta tarefa pode ser uma tarefa em si ou parte de uma tarefa maior. Nesta abordagem, no possvel saber a quem pertence a tarefa e se ela parte de uma tarefa maior. Por exemplo, um programa de usurio em execuo em um sistema multiprogramado suportando a preempo composto de vrias pequenas tarefas, cada uma realizada entre duas interrupes do processador. Em outras palavras, parte desta tarefa executada pelo processador, at que ocorra uma interrupo avisando que o tempo para aquela microtarefa se esgotou. O escalonador (outra tarefa) executa ento e decide qual microtarefa deve ser executada em seguida. Perde-se, assim, a noo de unidade que o modelo de processo apresenta. Naquele modelo, existe um processo escalonador, um processo para cada tarefa que envolve um programa em execuo. Na abordagem baseada em interrupes, tudo ocorre em funo das interrupes: perde-se a noo de um todo que o processo representa.

Running

Blocked

Ready

Figura 2. Diagrama de transio de estados de um processo [TAN92].

Algoritmos de Escalonamento. Na literatura, existem vrios algoritmos de escalonamento de processos. O mais simples adota uma poltica justa: o primeiro processo na fila de processos prontos

executa. Esta poltica respeita a ordem da fila e considera que processos no so preemptados (interrompidos). Apesar de simples, no comum. As polticas mais implementadas so baseadas em fatias de tempo (tambm chamada de quanta - plural de quantum de tempo) e em prioridades ou, mais comumente, uma mistura dos dois. Um processador executa um processo durante uma fatia de tempo. Ao trmino desta fatia, o processo desescalonado e passa para o final da fila de processos prontos para executar. Um processo naturalmente pode no usar toda a fatia disponvel: ele pode-se bloquear por algum motivo antes que a fatia termine. Esta estratgia baseada em fatias conhecida como round-robin. Em um esquema de prioridades, o prximo processo a ser executado aps um desescalonamento deve ter prioridade maior, sendo, eventualmente, um processo no preemptvel (interrompvel).

Implementao de Processos. A fim de implementar o modelo de processos, o sistema operacional mantm uma tabela chamada de tabela de processos, com uma entrada por processo. As principais informaes contidas nesta tabela so (tomando Unix como exemplo): informaes relativas gerncia de processos valor dos registradores contador de programa (PC - program counter) que contm o endereo da prxima instruo a ser executada palavra de status do programa endereo da pilha do programa estado do processo (executando, pronto para executar, bloqueado) momento em que o processo iniciou tempo utilizado de processador tempo de processador utilizado pelos processos filhos (processos gerados pelo processo corrente) endereos para fila de mensagens bits de sinais pendentes identificador do processo (PID - Process ID) informaes relativas gerncia de memria endereo do segmento de texto

endereo de segmento de dados inicializados endereo de segmento de dados no inicializados status de sada status de sinalizao identificador do processo (PID - Process ID) identificador do processo pai identificador do grupo do processo identificador real e efetivo do usurio identificador real e efetivo do grupo informaes relativas gerncia de arquivos mscara de acesso aos arquivos diretrio raiz diretrio corrente descritores de arquivos identificador efetivo do usurio identificador efetivo do grupo do usurio parmetros de chamadas ao sistema Cada processo tem uma entrada na tabela de processos contendo todas ou parte das informaes acima. Em geral, esta tabela fica residente em memria principal, cache ou mesmo em registradores especiais do processador, dependendo de caractersticas arquiteturais do computador. Quando um processo desescalonado pelo escalonador (perde a vez), todas as informaes pertinentes a este processo devem ser salvas na tabela de processos. O sistema operacional deve carregar, a partir da tabela de processos, o estado do prximo processo a carregar .

A tabela de processos mantm informaes sobre os processos. Mas o que o processo propriamente dito? Ora, um processo um programa em execuo que consome principalmente dois recursos: processador e memria. Instrues e dados devem residir em cache, memria principal e mesmo em memria secundria (disco magntico, normalmente) quando o sistema dispem de memria virtual. O espao de memria ocupado por um processo chamado de rea de trabalho (workspace) e contm basicamente (usando novamente o modelo Unix de processo):

1. um segmento de texto: contm as instrues a serem executadas em linguagem de mquina; 2. um segmento de dados: contm os dados do programa. composto de duas partes, um segmento de dados inicializados (em geral, constantes) e um segmento de dados no inicializados, cujo espao no alocado quando o processo inicia. Por esta razo, o segmento de dados no inicializados possui tamanho varivel (pode ocorrer alocao dinmica de memria); 3. um segmento de pilha (stack): contm variveis do ambiente de execuo, o string contendo a linha de comando e endereos de retorno de procedimentos.

A Figura 3 apresenta o espao de endereamento de um processo Unix em execuo. BSS corresponde rea de dados no inicializados de um processo Unix.

Pilha

BSS

Dados

Cdigo

Figura 3. Espao de endereamento de um processo tpico em Unix [TAN92].

Em um sistema multiprogramado, diferentes processos se alternam na utilizao do processador. O processador que executa um programa contm em seus registradores dados daquele programa,

eventualmente dados de controle (parte de tabelas), o contador corrente do programa e o espao de trabalho (completo ou parcial) na memria. A tabela de processos est tambm na memria. Quando a fatia de tempo correspondente quele processo termina (em um escalonamento baseado em fatias de tempo) ou quando uma operao causadora de bloqueio executada, o processo deve ser desescalonado. Isto implica em salvar o contexto deste processo, ou seja, salvar em memria todas as variveis de estado daquele processo. Em geral, isto implica em salvar contedo de registradores e tabelas. No necessariamente o espao de trabalho daquele processo sai da memria. Alis ele deve ficar, visto que, em caso de desescalonamento por trmino da fatia de tempo, o processo voltar a executar em breve. Se o processo bloqueado por alguma outra razo diferente do trmino da fatia de tempo, o processo colocado em estado de bloqueado, aguardando algum evento a fim de que ele se desbloqueie (por exemplo, fim de uma operao de entrada e sada).

Este salvamento de contexto possui tempos na ordem do milissegundo (se o salvamento for em disco) ou da ordem do microssegundo (se o salvamento for em RAM) e, se no for devidamente eficiente, pode comprometer o desempenho do sistema como um todo.

Suponha agora que tal sistema multiprogramado seja utilizado para programao concorrente e pseudo-paralela. Pseudo-paralela pois, inicialmente, considerado que se dispem de um nico processador. Com um processador, no se pode ter paralelismo real. Em um sistema monoprocessado, multiprogramado, vrios processos devem cooperar a fim de resolver um nico problema. Normalmente, um sistema multiprogramado suporta diferentes tarefas de diferentes usurios executando de maneira concorrente. Assim, temos uma edio de textos de um usurio X, em concorrncia com uma compilao de um usurio Y, em concorrncia com um shell (interpretador de comandos) de um usurio Z, e assim por diante. Nada impede de termos vrios processos resolvendo um nico problema de um nico usurio. Isto no parece to distante assim do quotidiano. Um ambiente de janelas normalmente funciona desta maneira. Cada janela tem um processo associado em execuo. Existe um processo pai, normalmente o shell de origem, que, a pedido do usurio, lana diferentes processos correspondentes a diferentes janelas. Tudo isto gerenciado por um programa de controle de janelas. A comunicao, em geral, ocorre dos diferentes processos filhos para o processo pai, com fins de notificao de eventos. O gerenciador de janelas deve-se manter informado de tudo o que ocorre na tela.

A mesma abordagem poderia ser utilizada para resolver um problema menos visual e mais cientfico, por exemplo, uma multiplicao de matrizes ou uma simulao numrica. O objetivo explorar o mximo de concorrncia possvel para ir mais rpido. Um processo que se bloqueia por entrada/sada pode ceder o processador para outro processo irmo que o est ajudando a resolver o mesmo problema. Em sistemas multiprogramados tradicionais, como Unix, apesar da cooperao, estes processos, para o sistema operacional, no so diferentes dos outros processos em execuo dos outros usurios. Suponha que dois processos A e B cooperem para resolver um dado problema arbitrrio X. Se A desescalonado, nada garantido se o prximo processo a ser executado B. Pode ser, pode no ser, depende do nmero de processos em execuo e, principalmente, da poltica de escalonamento adotada pelo sistema operacional. Eventualmente, uma prioridade mais alta pode ser concedida a A e a B, a fim de que eles, em concorrncia com o sistema operacional, sempre executem, antes de qualquer outro processo de outro usurio. Mas, isto envolve um acordo externo ao sistema (o super usurio pode modificar a prioridade de processos: um usurio comum consegue abaixar a prioridade de seu processo, jamais subi-la). Assim, alm de contar com um chaveamento de contexto nada diferente em relao ao chaveamento de contexto de outros processos, estes processos no compartilham o processador em conjunto.

Outro aspecto a ser considerado a separao das reas de trabalho entre os diferentes processos, sejam eles cooperantes ou no (independentes). Um processo no tem acesso rea de trabalho de outro processo. Se um processo A precisa de dados de outro processo B, o processo B pode, por exemplo, escrever o dado em um arquivo e, em seguida, o processo A ler o arquivo com o dado. Outra possibilidade o processo A enviar uma mensagem para B solicitando o dado e aguardar o processo B enviar para A uma outra mensagem com o dado solicitado. Estas so as duas maneiras bsicas para dois processos comunicarem (a ltima depende de um suporte do sistema operacional para troca de mensagens). Qualquer outra maneira corresponde a uma variao dos mtodos acima. A rea compartilhada para comunicao pode ser um registrador, memria principal ou disco (no caso de arquivos). Em ambientes monoprocessados, a troca de mensagem entre processos tambm ser feita atravs de uma memria compartilhada que conter a mensagem a ser transmitida. Porm, se os processos residem em dois processadores distintos, interconectados por uma rede, ento a mensagem trafegar pelo meio de interconexo (e.g., um barramento), constituindo uma mensagem, propriamente dita, enviada de uma mquina a outra. Quando dois processos se comunicam atravs de uma rea comum, o acesso a esta rea deve ser controlada de modo a permitir o acesso nico por

11

um processo. A posio de uma memria pode ficar inconsistente (e a instruo associada), se dois ou mais processos tentarem atualizar esta posio de memria concorrentemente. Situaes como esta podem levar s chamadas situaes de corrida. Um exemplo apresentado na Figura 4.

Para contornar este problema de compartilhamento, diferentes mecanismos podem ser implementados, tanto em hardware quanto em software, atravs de primitivas de baixo nvel ou primitivas mais abstratas. A literatura extensa [SIL94][TAN92]: instrues do tipo TSL (Test and Set Lock), primitivas sleep e wakeup, semforos, contadores de eventos, primitivas lock (tranca), monitores e mesmo mensagens (uma comunicao atravs de mensagens estabelece, por si, uma ordem de acesso ao dado: primeiro o dado enviado e posteriormente recebido, jamais o contrrio). Em algum momento, estas primitivas garantem uma atomicidade de execues de primitivas de sincronizao entre processos.

Processo 1 counter :=2; counter := counter+1; ....

Processo 2 counter := 0; counter := counter-1; .....

Se a operao counter := counter+1; decomposta em: registrador1 := counter; registrador1 := registrador1+1; counter := registrador1; e a operao counter := counter-1; decomposta em: registrador2 := counter; registrador2 : = registrador2-1; counter := registrador; o processo pode ser desescalonado entre qualquer uma das sub-instrues acima, levando a possveis diferentes valores de counter aps a execuo concorrente do processo 1 e do processo2.

Figura 4. Um exemplo de situao de corrida.

O esquema de processos fechados, comunicando-se atravs de memria compartilhada ou atravs de mensagens, adequado? Algumas vantagens e desvantagens podem ser levantadas:

Vantagens: o espao de trabalho de cada processo devidamente protegido pelo sistema operacional que no permite que outros processos (sejam cooperantes ou no) obtenham acesso a espaos de trabalho de outros processos; um processo que participa em um grupo de processos cooperantes pode, por algum motivo, morrer sem afetar fisicamente os outros processos. Os processos cooperantes podem ficar todos bloqueados caso um ou mais deles morram. Mas se a aplicao for tolerante a falhas, dentro do prprio programa, existem mecanismos que prevem este fenmeno. Os outros processos se adaptam em funo da ausncia do elemento falho.

Desvantagens: suponha dois processos A e B cooperando para a realizao de uma tarefa. Se o processo A, em execuo, se bloqueia por algum motivo, logo no incio da fatia de tempo a ele alocada, o outro processo B, que tambm auxilia a resolver o mesmo problema de A, provavelmente no ser escalonado para preencher o resto da fatia no utilizado por A. Como no possvel prever com antecedncia qual ser o prximo conjunto de instrues a ser executada por um processo, nada se pode afirmar sobre quando um processo perder o processador. Se se d 10 unidades de tempo para um processo se executar e ele s executa uma unidade de tempo destes 10, por que no dar as 9 unidades de tempo restantes para outro processo cooperante associado? se fosse possvel ordenar diferentes processos envolvidos na mesma computao de maneira consecutiva, o problema acima estaria resolvido. Porm, o tempo de chaveamento de contexto poderia ainda ser considerado alto. A pergunta que resta : no seria possvel reduzir o tempo de chaveamento de contexto destes processos, sabendo que estes processos cooperam? Ser que, pelo fato deles cooperarem, alguma simplificao na implementao destes processos no poderia ser idealizada? uma forma de comunicao entre processos passa por um arquivo. Mesmo quando isto no explcito (por exemplo, usando o pipe do Unix), o sistema de arquivos acionado. Um acesso a arquivo , em geral, muito mais lento do que um acesso puro memria.

13

Em face destas vantagens e desvantagens, duas solues poderiam ser adotadas:

1. reduzir o tempo de chaveamento de contexto e implementar mecanismos mais eficientes para comunicao entre processos 2. permitir que um processo lance outros processos com reas de acesso comuns para comunicao e que estes processos filhos compartilhem em grupo a mesma fatia de tempo alocada ao processo pai

importante observar que a criao de processos uma tarefa realizada com muita frequncia. O problema que os processos no compartilham nada entre si, quando muito, o segmento de texto (cdigo), que , por natureza, no modificvel na maior parte dos casos. Existem esforos em direo primeira soluo: manter a proteo existente entre processos e realizar um chaveamento de contexto eficiente. O hardware pode auxiliar nesta operao. Resta resolver o problema do compartilhamento da fatia de tempo entre processos correlatos. Isto pode ser imposto automaticamente pelo sistema operacional, modificando a estratgia de escalonamento do sistema operacional e criando um campo na tabela de processos que indica se o processo participa de um grupo de processos cooperantes.

Com tantas modificaes, um novo tipo de processo pode ser idealizado: o processo leve, tambm conhecido como thread. o que ser visto em seguida.

2. Processos Leves (Threads)

Segundo Feitelson e Rudolph [FR90], um sistema paralelo deve suportar dois tipos de modelos de processos. Os processos, ditos pesados, so aqueles mencionados na seo anterior. Eles permitem um projeto estruturado e modular de grandes sistemas, criando contextos distintos para clculos independentes, separados e protegidos uns dos outros. Isto naturalmente vlido para usurios independentes. Threads permitem um paralelismo de granularidade mais fina; muitas threads podem existir dentro de um contexto de um processo, cooperando entre si a fim de realizar um dado clculo e compartilhando o espao de endereamento, arquivos abertos, etc. [FR90]. Por granularidade mais fina, subentende-se que a unidade de clculo realizada de maneira concorrente menor do que a unidade de clculo associada a um processo. Por exemplo, a granularidade de um processo de

programa. A granularidade de uma thread pode ser de um procedimento dentro de um programa: isto , procedimentos podem ser executados concorrentemente.

A Figura 5 apresenta graficamente a diferena entre estas duas abstraes. Thread

Contador de Programa

Processos Pesados

Figura 5. Processos e threads [TAN92].

importante observar que um processo pesado visto anteriormente composto de apenas uma thread (vide Figura 5). Por exemplo, um processo que corresponde execuo de um programa compilado em C, possui uma thread de controle que comea no main(), passa por todas as instrues do programa, inclusive instrues embutidas em procedimentos, e termina no ltimo fecha-chaves do programa (fechando o bloco iniciado pelo main). Threads podem ser conhecidas tambm como processos leves. Em ambientes multiprocessados, diferentes threads podem ser executadas realmente em paralelo em diferentes processadores.

Threads tm-se tornado populares porque possuem algumas caractersticas de processos pesados, mas podem ser executadas mais eficientemente [SIL94].

Histrico. A noo de uma thread, como um fluxo sequencial de controle, data de 1965, pelo menos, com o Berkeley Timesharing System. Naquela poca, eram chamados de processos e no de threads. Processos interagiam atravs de variveis compartilhadas, semforos e mecanismos anlogos. Max Smith desenvolveu um prottipo de implementao de threads sobre o sistema operacional Multics em 1970. Ele utilizou mltiplas pilhas em um processo pesado para suportar compilaes em background. Talvez o ancestral mais importante das threads a primtiva suportada pela linguagem PL/I, de cerca de 1965. A linguagem como definida pela IBM proporcionava uma chamada do tipo:

15

CALL XXX (A, B) TASK;

que criava uma thread para XXX. No est claro se os compiladores da IBM implementaram esta possibilidade, mas foi examinada em detalhes quando do desenvolvimento de Multics. Foi decidido que a chamada TASK como definido no mapeava em processos, desde que no havia proteo entre threads de controle. Assim, Multics tomou uma direo diferente, e a chamada TASK foi removida de PL/I pela IBM. Em seguida, surgiu Unix no incio dos anos 70. A noo Unix de um processo transformou-se em uma thread nica de controle sequencial mais um espao de endereamento virtual (incidentalmente, a noo Unix de processo derivou diretamente dos processos no projeto do Multics). Assim, processos em Unix so mquinas assaz pesadas, desde que eles no podem compartilhar memria entre si (cada processo tem o seu espao de endereamento, podendo comunicar-se atravs de pipes ou de mensagens). Aps um certo tempo, usurios de Unix comearam a sentir falta dos velhos processos que compartilham memria. Isto levou s threads como as conhecemos hoje. O termo leves (lightweight) surgiu em finais da dcada de 70 ou incio da dcada de 80, junto com os primeiros microkernels (Thot, Amoeba, Chorus, Mach). Como observao, colocado que threads tm sido utilizadas em aplicaes de telecomunicaes por um longo tempo [NWS96]. O FAQ (Frequently Asked Questions - questes frequentemente colocadas: documento de um newsgroup Internet com as dvidas mais comuns dos leitores daquele newsgroup) no comenta sobre a linguagem Algol (pelo menos da sua implementao nas mquinas de pilha da Burroughs) na qual threads concorrentes eram disparadas dentro de aplicaes, sejam como co-rotinas (veja abaixo), sejam como threads assncronas.

Implementao de threads. Cada processo leve deve ter seu prprio contador de programa e sua prpria pilha. Podem conter tambm uma rea de dados privados. Threads podem criar outras threads e, como processos pesados, podem bloquear-se na espera de um evento. Threads compartilham o processador, da mesma maneira que processos pesados. Porm, diferentemente de processos pesados, uma thread bloqueada pode ceder o processador a outra thread do mesmo processo. Embora tenham seu prprio PC, sua pilha e dados privados, as threads se executam sobre um mesmo espao de endereamento, compartilhando variveis globais se for o caso. Segundo Tanenbaum, no h proteo alguma entre threads, pois (1) impossvel (visto que todas elas atuam dentro do espao de um nico processo), e (2) em geral, no necessrio, pois estes processos leves

esto exatamente cooperando para resolver um problema comum e pertencem a um mesmo usurio. Threads possuem estados tambm, naturalmente: em execuo, prontas para executar, bloqueadas e terminadas. Dentro deste modelo, necessrio caracterizar um processo leve como terminado, visto que outras threads podem estar em execuo e o espao de trabalho deixado pela thread que terminou ainda no tenha sido recolhido pela thread pai.

Uma tabela de threads deve ento ser mantida tambm. Os itens por thread so normalmente: o contador de programa; o endereo da pilha; o conjunto de registradores associados; endereos das threads filhas; estado. Resta para o processo, como um todo, informaes do tipo endereo da rea de trabalho, variveis globais, apontadores para informaes de arquivos abertos, endereos de processo filhos, informaes sobre timers, sinais, semforos e de contabilizao.

Threads podem ser sncronas ou assncronas. Quando elas so sncronas, elas executam at que elas mesmas decidam no continuar a execuo (ou termine a fatia de tempo para aquele processo que, ao voltar a executar, continuar executando a mesma thread interrompida). Isto facilita o mecanismo de compartilhamento de dados, pois jamais uma thread ser interrompida, se ela no o quiser explicitamente. Threads assncronas, por outro lado, podem executar umas com as outras, por exemplo, subdividindo a fatia de um processo equitativamente entre as threads ativas (no bloqueadas).

Modelos de utilizao de threads. O padro de comportamento das threads dentro de um processo pode ser o mais variado possvel. Por exemplo, Tanenbaum apresenta trs organizaes diferentes: a mestre/escravo, um modelo baseado em time ou equipe e um modelo baseado em pipeline. Na primeira configurao, existe uma thread mestre que recebe tarefas a serem realizadas e que as despacha para outras threads que efetivamente realizaro as tarefas (uma por thread, por exemplo). Um servidor de arquivos pode ser estruturado desta maneira. Existe uma thread que recebe pedidos

17

de abertura de arquivos. Para cada arquivo, o mestre designa uma thread escrava que se encarregar de gerenciar um arquivo cuja abertura foi solicitada. aquela thread escrava que ler ou escrever no arquivo a qual ela responsvel. No modelo de time, possvel imaginar threads com habilidades diferentes: por exemplo, uma somente l arquivos, outra somente realiza clculos com nmeros inteiros, outra acol somente realiza operaes sobre nmeros com ponto flutuante. Da mesma maneira que uma equipe de operrios pode levantar uma casa, as diferentes threads podem resolver um dado problema. O modelo em pipeline (ou em duto) anlogo a uma linha de montagem, onde diferentes threads, tambm especializadas, realizam diferentes operaes sobre dados que so passados de thread em thread, como um carro sendo montado em uma linha de produo.

Pacotes de threads. As primitivas de manipulao de threads so em geral disponveis atravs de bibliotecas ou de pacotes (thread packages). Um primeiro problema a ser abordado se threads so criadas estatica ou dinamicamente. No caso esttico, o nmero de threads definido em tempo de redao ou de compilao do programa. A pilha alocada para cada thread de tamanho fixo. No caso dinmico, que o mais habitual, threads so criadas sob demanda pelo programa. Quase sempre, o nome da primitiva de criao de threads envolve o termo fork, tambm bastante utilizado para criao de processos pesados. Entre diversos parmetros, os mais importantes so o nome do procedimento que est sendo associada a thread, parmetros para o procedimento, tamanho da pilha e at mesmo uma prioridade de escalonamento, prioridade est vlida somente dentro do contexto do processo com a thread pai. possvel criar um esquema de prioridades entre threads de um mesmo processo. O processo, por sua vez, possui uma prioridade externa usada pelo escalonador do sistema operacional. A prioridade do processo, como vimos, em geral no modificada pelo usurio. Assim como processos, threads podem terminar normalmente com a execuo do fim do procedimento, ou podem ser mortas por outras threads.

Threads podem comunicar-se atravs das variveis globais do processo que as criou. A utilizao destas variveis pode ser controlada atravs de primitivas de sincronizao (monitores, semforos, ou construes similares). Primitivas existem para bloqueio do processo que tenta obter acesso a uma rea da memria que est correntemente sendo utilizada por outro processo. Primitivas de sinalizao de fim de utilizao de recurso compartilhado tambm existem. Estas primitivas podem acordar um ou mais processos que estavam bloqueados. importante observar que variveis globais, embora teis em alguns contextos, devem ser evitadas em outros. Valores de status de

operaes de entrada/sada so em geral armazenadas em variveis globais. O exemplo de [TAN92] a varivel errno que global a todas as threads. Se uma thread realiza uma abertura de arquivo para leitura e algum problema ocorre (e.g., o arquivo no existe), a varivel errno conter um cdigo de operao invlida. Porm, antes de test-la para tomar alguma providncia, a thread desescalonada e outra thread passa a ser executada pelo processador. Esta mesma thread realiza outra operao de entrada e sada com sucesso e a mesma varivel errno conter o valor de um cdigo vlido de status. Quando a outra thread voltar a executar ela enxergar um valor errneo da varivel status.

Implementao de pacotes de threads. [TAN92] apresenta duas maneiras possveis de implementar threads: uma no espao do usurio e outra no espao do sistema operacional. Na primeira opo, o kernel do sistema operacional no tem conhecimento da existncia das threads. Para o kernel, existem processos pesados que so executados intercaladamente. A vantagem que pacotes deste tipo podem ser utilizados em sistemas operacionais que no suportam threads (por exemplo, Unix padro como SunOS). O escalonamento das diferentes threads gerenciado por uma camada de software abaixo das threads e acima do kernel. Chamadas de sistema no so realizadas: toda a gerncia da execuo das threads feita pelo run time system (veja Figura 6). Outras vantagens so a flexibilidade do escalonamento (o usurio pode at mesmo programar o seu prprio algoritmo) e a extensibilidade ou escalabilidade (scalability): se as threads fossem todas gerenciadas pelo kernel, este se tornaria um gargalo e o desempenho cairia com o aumento do nmero de threads.

Em pacotes de threads gerenciados pelo sistema operacional, no h necessidade de uma camada adicional de software. Para cada processo ativado, existe uma tabela de threads com informaes sobre o seu estado. Esta tabela tambm existe no caso de pacotes a nvel de usurio, mas a mesma agora se encontra em um espao de trabalho do sistema operacional. Qualquer operao relativa a uma thread implementada com uma chamada de sistema, o que envolve um overhead maior. Quando uma thread se bloqueia, o sistema pode executar outra thread do mesmo processo ou uma thread de outro processo.

19

Threads
User Space Run time system Kernel User-level threads package Kernel Space

Threads

Kernel Kernel-level threads package

Figura 6. Pacotes de threads a nvel de usurio e a nvel de sistema [TAN92].

Comparando as duas abordagens, observa-se que threads a nvel de usurio jamais podem bloquear. Se uma delas bloqueiam, todas as outras ficam bloqueadas e o processo como um todo desescalonado. Uma soluo seria dispor de chamadas de sistemas no bloqueantes, mas isto envolveria mudanas no sistema operacional subjacente. Pode-se descobrir antes de executar uma instruo se ela vai bloquear ou no (algo como um comando do tipo probe - ou sonda). Se uma instruo vai bloquear, o pacote pode ento decidir executar outra thread. Este comando de sonda implica em modificaes na biblioteca de chamadas de sistema. Jacket o nome dado ao cdigo associado em torno de uma chamada de sistema a fim de fazer esta verificao. Escalonamento round-robin no possvel entre threads executando dentro de um quantum de tempo do processador, pois interrupes de relgio no ocorrem neste intervalo (a no ser que seja explicitamente solicitada tal interrupo). Isto quer dizer que se uma thread comea a executar, ela o far at terminar a fatia de tempo alocada para o processo como um todo ou a thread explicitamente se bloquear ou passar para o estado ready. Threads so necessrias em aplicaes onde uma concorrncia pode ser explicitada e ocorra muitos bloqueios (por exemplo, uma aplicao I/OBound, que realiza muita entrada/sada e pouco clculo). Outros problemas podem ocorrer para ambos os casos: problema de cdigo de biblioteca compartilhado e dados globais por processo.

Reentrncia. O desenvolvimento de aplicaes com mltiplas threads requer um ambiente com suporte a compartilhamento de cdigo (ou reentrncia) entre threads. Solaris, por exemplo, proporciona verses reentrantes para a maioria das bibliotecas comumente utilizadas. Correntemente, Solaris no proporciona verses seguras a threads das bibliotecas Motif e

OpenLook, que so raramente utilizadas por mltiplas threads em um programa. Windows NT tambm proporciona verses reentrantes para a maioria de suas bibliotecas de uso comum.

Depurao. A depurao de aplicaes multithreaded um grande desafio e pode ser frustrante sem o suporte de um depurador ciente de threads. Solaris suporta um depurador multithreaded como parte de seu ambiente SPARCworks/iMPact, enquanto o depurador NT multithreaded faz parte de Visual C++. Alm de mostrar as threads por processo, ambos os depuradores suspendem e resumem threads e inspecionam variveis por thread.

Co-rotinas. Threads que no so preemptiveis e podem somente ser um nico fluxo ativo de controle dentro de um processo (no importando o nmero de processadores disponveis) so referidas como co-rotinas. Programao com co-rotinas requer uma abordagem bem diferente da programao baseada em threads. Isto porque os problemas de sincronizao e de compartilhamento de recursos que ocorrem em ambientes com threads no perturbam o programador de co-rotinas.

Threads e Sistemas Distribudos e Concorrentes. As threads so de interesse particular em sistemas distribudos e concorrentes. Como vimos, mdulos em sistemas concorrentes so, em geral, mais complexos do que mdulos em sistemas sequenciais. Por exemplo, um procedimento pode estar associada a uma ou mais threads concorrentes que interagem atravs de uma memria comum ou atravs de passagem de mensagens. Assim, uma relao simples de entrada e sada (caracterstica de procedimentos de programas sequenciais) no adequada para descrever o comportamento de um procedimento, desde que os seus estados intermedirios internos podem estar visveis aos clientes atravs de outras interaes. Interfaces em procedimentos concorrentes incluem no somente pontos de entrada e de retorno, mas tambm pontos intermedirios que podem interagir com outras threads. Mdulos em sistemas concorrentes podem tambm ser ativos: eles podem ter threads internas em background, cujo efeito em algum lugar deve ser descrito em uma especificao [WEI93].

Futuro. Outros sistemas operacionais so multithreaded: NextStep, OS/2, AIX (e outros Unix), e Windows95. Verses futuras do sistema operacional de Macintosh sero tambm multithreaded.

3. Exemplos

21

A seguir sero apresentados exemplos de sistemas operacionais e linguagens que suportam o conceito de threads. Sero apresentados aspectos de implementao e, em seguida, exemplos de utilizao.

3.1 Solaris

Histrico. Solaris a nova denominao dada, a partir de 1992, aos sistemas operacionais Unix das estaes de trabalho Sun (anteriormente, eram conhecidos como SunOS - a verso 4 do SunOS corresponde ao Solaris 1). A ltima verso de Solaris a 2.5.1, capaz de ser executada, pela primeira vez, em outras plataformas diferentes das estaes Sun (UltraSPARC/SPARC). Esta verso est disponvel tambm para os microprocessadores Intel e PowerPC.

Objetivos. Solaris 2 um sistema operacional que suporta threads a nvel de sistema e a nvel de usurio, multiprocessamento simtrico e suporte para aplicaes de tempo real. Com a verso 2.5.1, este objetivo alcanado considerando diferentes tipos de arquiteturas (SPARC, Intel, PowerPC).

Implementao. As bibliotecas de threads a nvel de usurio suportam basicamente a criao e o escalonamento de threads e o kernel no toma conhecimento destas threads. Entre as threads dos usurios e a dos kernel, existe um nvel intermedirio correspondente ao, no contexto de Solaris 2, chamado de processos leves (lightweight processes - LWP). Cada processo em Solaris 2 contm ao menos um processo leve. Estes processos leves so manipulados pela biblioteca thread. Threads a nvel de usurios so multiplexadas por LWPs do processo. Estas threads dos usurios podem ou no estar acopladas a uma LWP (serem bound ou unbound). Se elas no estiverem acopladas a uma LWP, nenhum trabalho pode ser realizado. Assim, threads a nvel do usurio podem disputar por uma LWP. Por outro lado, instrues dentro do kernel so executadas por threads a nvel de sistema. A cada LWP, existe uma thread do sistema e podem existir threads do sistema que trabalham em prol do sistema operacional e no possuem LWP associada (por exemplo, uma thread que serve pedidos de disco). As threads do kernel so efetivamente os nicos objetos escalonveis no sistema. Solaris 2 suporta multiprocessamento, ento diferentes threads podem executar-se sobre diferentes processadores. Threads podem obter exclusividade sobre um processador: este processador somente executa esta thread (veja Figura 7).
User-Level Thread

Lightweight Process

Kernel Thread

Kernel CPU

Figura 7. Threads em Solaris 2 [SIL94].

Qualquer processo pode ter vrias threads a nvel de usurio. Estas threads a nvel de usurio podem ser escalonadas e controladas (de maneira alternada) entre diferentes LWP sem interveno do kernel. No h chaveamento de contexto global quando uma thread se bloqueia e outra continua a executar, de modo que threads a nvel de usurio so bastante eficientes. Para estas threads a nvel de usurio, as LWPs somente so necessrias quando aquelas precisam comunicar-se com o kernel. Se existem, por exemplo, 5 threads bloqueadas por uma leitura em disco, ento devem existir 5 LWPs. Se existem somente 4 LWPs, uma thread do usurio espera o trmino de uma operao de leitura de outra thread. Ela se bloqueia no porque fez uma chamada de sistema, mas porque no existe LWP livre para execut-la. Uma LWP contm um bloco de controle de processo com dados de registradores, informao de contabilizao e de utilizao de memria. O chaveamento entre LWPs mais lento do que o chaveamento entre threads do kernel. Uma thread a nvel de usurio somente necessita de uma pilha e um contador de programa.

Se uma thread do kernel se bloqueia, a LWP associada (se existir alguma) ficar bloqueada tambm, assim como a thread do usurio associada. Uma thread do kernel possui somente uma pequena estrutura de dados e uma pilha. O chaveamento entre threads do kernel no requer a mudana de informaes de acesso memria, e portanto relativamente rpido.

23

Interface. As threads em Solaris possuem uma API (Application Programming Interface) baseada na interface do Unix International, e suporte para interface Posix para threads est disponvel no Solaris 2.5. A interface Solaris muito semelhante interface Posix, e aplicaes desenvolvidas usando a API de Solaris podem ser facilmente portadas para usar a interface Posix. Identificadores de threads em Solaris so garantidos nicos dentro do contexto do processo.

Exemplos. apresentado um extrato de um programa que implementa o produtor-consumidor [JUN96]. Neste problema, existem um processo que produz um dado (no exemplo, inteiros) em um buffer de tamanho limitado (no exemplo, 10 posies) e um processo que l dados do buffer e que faz um tratamento arbitrrio sobre o dado lido. Um produtor no produz quando o buffer est cheio e o consumidor no consome quando o buffer est vazio. Alm disto, o acesso ao buffer (para leitura e excritura) deve ser exclusivo.
#include <thread.h> #include <synch.h> #include <stdlib.h> #define MAXTAM 10 sema_t *mutex; sema_t *empty; sema_t *full; int buffer[MAXTAM]; int nextp, nextc; void *Produtor(void *arg) { int item; int f; for(f = 1; f < 10*MAXTAM; f++) { item = rand(); sema_wait(empty); buffer[nextp] = item; sema_post(full); nextp++; nextp = nextp % MAXTAM; } } void *Consumidor(void *arg) { int item; int f; for(f = 1; f < 10*MAXTAM; f++) { sema_wait(full); item = buffer[nextc]; sema_post(empty); /* incluso da biblioteca thread.h */

nextc++; nextc = nextc % MAXTAM; } }

void main() { int valor; thread_t *consum; /* aloca espao para os semforos */ mutex = (sema_t *) malloc(sizeof(sema_t)); empty = (sema_t *) malloc(sizeof(sema_t)); full = (sema_t *) malloc(sizeof(sema_t)); /* inicializa semforos */ sema_init(mutex, 1, USYNC_THREAD, NULL); sema_init(full, 0, USYNC_THREAD, NULL); sema_init(empty, MAXTAM, USYNC_THREAD, NULL); nextc = nextp = 0; /* define o nvel de concorrncia */ printf("Nivel de concorrencia %d\n", thr_getconcurrency()); printf("Novo valor: "); scanf("%d", &valor); thr_setconcurrency(valor); printf("Novo valor: %d\n ", valor); consum = ( thread_t *) malloc(sizeof(thread_t)); /* lana threads */ thr_create(NULL, 0, Produtor, NULL , 0, NULL); thr_create(NULL, 0, Consumidor, NULL , 0, consum); thr_join(*consum, NULL, NULL); }

As funes de gerncia de threads comeam com o prefixo thr_. Os dois procesimentos so lanados pelo procedimento thr_create cujos parmetros so: base da pilha (default NULL); tamanho da pilha (quando vale 0, pega o tamanho default - 1 Megabyte); nome do procedimento; argumentos; flags (define o estado e tipo da thread. Por exemplo, THR_SUSPENDED quando criada em estado suspenso); novo ID da thread (o procedimento produtor no tem ID. O procdeimento consumidor tem o ID armazenado na varivel consum).

25

3.2 Windows NT

O sistema operacional Microsoft Windows NT foi projetado desde o incio para suportar multiprocessamento e multithreading (suporte s threads). A partir de conceitos de orientao a objetos, Windows NT usa classes de objetos para representar os recursos do sistema. Em Windows NT, instncias da execuo de um programa so tambm chamadas de processos. Um processo possui seu prprio espao de endereamento e recursos (memria, arquivos abertos e, mais tipicamente, janelas). A chamada de criao de processo CreateProcess, com a qual uma thread automaticamente construda para um processo. A criao de threads adicionais realizada atravs da rotina CreateThread. A thread recm-criada inicia executando uma rotina especificada por um parmetro de CreateThread. Cada thread em NT tem sua prpria pilha (de usurio e de sistema), sendo que o tamanho da pilha desta recm-criada thread pode ser tambm especificada na rotina CreateThread. Processos e threads so representados como objetos. Ao contrrio de Solaris, NT usa um mapeamento um-para-um entre threads do usurio e threads do sistema.

Threads podem ser de tempo real e variveis. Threads em tempo real em NT so sempre escalonadas antes das outras threads do sistema e o kernel NT no altera as prioridades das threads de tempo real. Threads de tipo varivel tm uma prioridade dinmica e uma base. A prioridade de base de uma thread varia dois nveis acima ou abaixo da prioridade de base do processo. O kernel periodicamente ajusta a prioridade dinmica da thread. Por exemplo, quando uma thread espera por um I/O, o kernel aumenta a prioridade dinmica daquela thread. Threads que so CPU-bound tendem a ter prioridades dinmicas mais baixas. A prioridade dinmica da thread jamais cai abaixo da prioridade de base desta thread. Cada processo tem um grau de afinidade por processadores, um conjunto de processadores sobre os quais as threads daquele processo podem executar. Este grau de afinidade pode afetar o escalonamento das threads. NT, como Solaris, emprega uma afinidade soft: ele sempre tenta escalonar uma thread sobre o processador no qual ele executou por ltimo.

Implementao. Uma thread em NT uma unidade de execuo que inclui um conjunto de instrues, valores relacionados de registradores da CPU e uma pilha. Uma thread executa no espao de endereamento de um processo e utiliza os recursos alocados para este processo. Em NT, uma thread deve ter um ID, registradores contendo o estado do processador, duas pilhas (uma para o modo privilegiado do processador e uma em modo usurio) e uma rea de armazenamento

privado. Threads em NT tm 32 nveis diferentes de prioridade (16 nveis para tempo real, 15 para varivel e 1 para o sistema). O escalonador (chamado em [PRA95a] de dispatcher) usa um algoritmo de escalonamento preemptivo baseado em prioridades. A thread com mais alta prioridade escolhida para executar. Threads podem mudar de prioridade atravs do procedimento SetThreadPriority. Threads NT podem ser suspensas ou resumidas (ou seja, continuadas aps serem suspensas) atravs de chamadas SuspendThread e ResumeThread. Uma thread pode ser criada em um estado suspenso. Uma thread pode terminar em uma das seguintes maneiras: fim do procedimento da rotina associada thread; chamada da funo ExitThread; trmino causado por outra thread chamando procedimento TerminateThread. Quando uma thread termina, o objeto thread torna-se sinalizado: todas as outras threads esperando que aquela thread termine so notificadas. Uma thread em espera pode determinar o status de sada de uma thread terminada atravs da funo GetExitCodeThread.

Cada thread tem um nico identificador que pode ser recuperado chamando GetCurrentThreadId Em aplicaes 32 bits para Windows NT, porm, alm do identificador de thread, necessrio um handle do objeto. Identificadores de threads em NT so nicos a nvel de sistema.

Escalonamento. Como apresentado acima, o escalonador preemptivo. Quando uma fatia de tempo pr-determinada e especfica termina, o escalonador tira o processador da thread que est executando. O escalonador pode escalonar uma outra thread para executar antes do fim da fatia da thread corrente ter terminado em uma das seguintes condies: a thread em execuo chama a funo Sleep; a thread se bloqueia chamando uma funo que causa que a mesma espere - por exemplo, esperando por um dispositivo de I/O ou esperando por um objeto sncrono ser sinalizado; uma thread de mais alta prioridade se torna disponvel. importante observar que a unidade de escalonamento uma thread. O escalonador do NT no escalona processos na maior parte do tempo. As prioridades so controladas nos nveis de processo

27

e de thread. Como citado acima, existem duas grandes classes de prioridade: variveis e de tempo real. As prioridades variveis podem ainda ser: ociosa (Idle); normal (Normal); alta (High). Dentro de cada classe, existem sete nveis. Cada thread tem uma prioridade de base que relativa classe de prioridade do processo. A preempo til para sistemas tais como de tempo real, onde imperativo para threads de mais alta prioridade estarem prontas para executar. A preempo implica em uma sobrecarga devido ao chaveamento de contexto frequente, de modo que bastante importante que a prxima tarefa esteja pronta para executar o mais rpido possvel. Em sistemas no preemptivos (ou cooperativos) como o Microsoft Windows 3.11, todos os outros processos

esperam que o processo em execuo libere o processador voluntariamente. No processamento cooperativo, uma aplicao mal desenvolvida pode monopolizar o processador (Figura 8).

Preemptivo (Windows NT)


Thread 1 Thread 2

Cooperativo (Windows 3.1)


Thread 1 Thread 2 Em execuo Chaveamento Invonluntrio Chaveamento Voluntrio

Figura 8. Processamento Preemptivo e Cooperativo.

O sistema operacional preempta uma thread depois que a fatia de tempo acabou ou quando uma thread de mais alta prioridade se torna pronta para executar. Uma thread voluntariamente libera o

processador se ela vai para um estado de espera, completa execuo ou se torna uma thread de mais baixa prioridade. Threads preemptadas so colocadas na fila de processos prontos para executar de sua prioridade. Uma regra simples que as n threads executveis mais prioritrias esto sempre executando, onde n o nmero de processadores.

Sincronizao. Threads sincronizam-se atravs de Mutexes Lock (semforos binrios), de objetos de tipo regio crtica , objetos de evento, objetos de semforos e operaes atmicas sobre inteiros.

Estados de threads NT. Uma thread NT pode estar em um dos seguintes estados em um dado tempo: esperando por um evento especificado ocorrer (no pode executar), pronto para executar e esperando por um processador disponvel, ou executando em um processador.

I/O assncrono. Windows NT 3.5 suporta um mecanismo chamado portas I/O-completion. Estas portas so projetadas para manipular I/O assncrono ou sobreposto (concorrente). A funo CreateIoCompletionPort associa uma porta com uma coleo de handles de arquivos, e a porta atua como um ponto de sincronizao. Quando uma operao de entrada/sada pendente sobre qualquer um dos handles de arquivo completa, um pacote de IO-completion ento enfileirado para aquela porta. Um nmero de threads de trabalho pode gerenciar entrada/sada para clientes chamando GetQueuedCompletionStatus para esperar sobre a porta do tipo I/O-completion. Estas portas tm controles de concorrncia embutidos. O kernel tenta limitar o nmero de threads executveis associadas a uma porta, nunca excedendo o valor de concorrncia da porta (que especificada quando a porta criada). Quando uma thread chama GetQueuedCompletionStatus, ela retorna quando o I/O est disponvel. Quando uma das threads associadas com uma completion port est bloqueada, o kernel seleciona uma thread em espera sobre a completion port para executar. Desta maneira, o sistema no fica sobrecarregado com threads executveis. Threads que bloqueiam em uma completion port so acordadas em uma ordem LIFO (ltima que chega, primeira que sai), enquanto os pedidos de I/O so tratados em uma ordem FIFO (primeiro que chega, primeiro que sai). Threads em execuo - aps completar uma transao - pode pegar o prximo pedido sem causar uma mudana de contexto. I/O-completion ports funcionam eficientemente sobre qualquer carga, seu desempenho no sofre com um trfego pesado.

29

Interface. Windows NT no suporta a interface Posix, e aplicaes usam a interface Win32 para desenvolver aplicaes multithreaded.

Exemplo. Apresentamos a seguir um extrato de um programa que ilustra a criao de uma thread. O procedimento a ser lanado como thread chama-se ThreadFunc.

DWORD ThreadFunc (LPDWORD lpdwParam) { printf (ThreadFunc: thread parameter=%d\n, *lpdwParam); return 0; } DWORD main (void) { DWORD dwThreadId, dwThrdParam = 1; HANDLE hThread; hThread = CreateThread ( NULL, /* nenhum atributo de segurana */ 0, /* use o tamanho default do stack */ (LPTHREAD_START_ROUTINE) ThreadFunc, /* o procedimento a ser lanado */ &dwThrdParam, /* argumento da funo */ 0, /* usar flags default de criao */ &dwThreadId); /* devole o id da thread */ if (hThread == NULL) ErrorExit (CreateThread error\n); ... Os atributos de segurana incluem um flag que determina se o handle pode ser herdado ou no por threads filhas. Os atributos de segurana tambm incluem um descritor de segurana, que o sistema usa para realizar verificaes de acesso em todos os usos subsequentes do handle da thread antes que o acesso seja concedido. Por outro lado, um flag de criao permite a criao de uma thread em estado suspenso, com a thread no executando at que a funo ResumeThread seja chamada.

3.3 Java

Conceitos Bsicos. Java uma linguagem multithreaded, i.e., ela prov suporte para a execuo de vrias threads que podem tratar diferentes tarefas simultaneamente. O suporte s threads da linguagem Java tambm inclui um conjunto de primitivas de sincronizao. Tais primitivas so baseadas no paradigma de monitores e variveis de guarda, um esquema amplamente difundido; concebido por [HOA78]. Existem dois mecanismos para a criao de threads em Java: implementando uma interface ou estendendo uma classe. A extenso de uma classe o mecanismo pelo qual mtodos e variveis so herdados de uma superclasse. A linguagem Java no suporta herana mltipla e portanto uma classe pode estender ou herdar mtodos e variveis de uma nica classe. Esta limitao da linguagem Java pode ser tratada atravs da implementao de interfaces, que a maneira mais comum de criar threads. As interfaces provm uma mecanismo pelo qual programadores definem o esqueleto de uma classe, definindo um conjunto de regras de um determinado tipo abstrato. Existem algumas diferenas bsicas entre uma classe e uma interface. Primeiramente, uma interface s pode conter mtodos abstratos1 ou constantes (variveis com clusulas static e final)2. J uma classe pode implementar mtodos e conter variveis que no sejam constantes. Uma interface no pode implementar mtodos. Uma classe que implementa uma interface deve implementar todos os mtodos declarados em uma interface. Interfaces tm a habilidade de estender outras interfaces e, diferentemente de classes, podem estender mltiplas interfaces. O primeiro mtodo de criar uma thread simplesmente estender a classe Thread. Tal procedimento s recomendado se a classe que deve ser executada com uma thread no precise nunca estender outra classe. A classe Thread definida no pacote java.lang, que deve ser importado dentro do mdulo que contenha uma classe que implemente uma thread. Considere o seguinte exemplo:

import java.lang.*; public class Contador extends Thread { public void run() { ... }
Um mtodo abstrato define o prottipo de uma funo, atravs da declarao de seu nome, do nmero e tipo de cada argumento e de seu tipo de retorno. 2 A clusula static aplicada a uma varivel define que somente uma instncia desta varivel existir, independente do nmero de classes instanciadas. A clusula final impede uma redefinio de uma varivel por uma subclasse. Deste modo a linha seguinte define uma constante PI em Java com valor 3,14: static final int PI = 3.14;
1

31

} Este exemplo cria uma classe Contador que estende a classe Thread do sistema e sobrescreve o mtodo Thread.run() para sua prpria implementao. A mesma classe pode ser criada implementando a interface Runnable, como no exemplo a seguir.

import java.lang.*; public class Counter implements Runnable { Thread T; public void run() { ... } } Neste exemplo, o mtodo abstrato run() definido na interface Runnable e est sendo implementado. importante notar a presena de uma instncia de uma classe Thread como uma varivel da classe Counter. A nica diferena entre os dois mtodos a maior flexibilidade existente na criao de uma classe Counter. No exemplo acima, existe ainda a possibilidade de estender a classe Counter de uma superclasse, se necessrio. Deste modo, a maioria das classes criadas como uma thread implementaro a interface Runnable desde que estas provavelmente estaro estendendo funcionalidade de uma alguma outra superclasse. A interface Runnable j existe na linguagem Java. interessante notar, entretanto, que tal interface no executa trabalho algum, contendo apenas um nico mtodo abstrato, como pode ser observado no cdigo a seguir extrado do cdigo fonte da linguagem Java.

package java.lang; public interface Runnable { public abstract void run(); } Isso tudo o que existe na interface Runnable. Uma interface s fornece o esqueleto o qual classes devem implementar. Neste caso, tal interface fora a definio de um mtodo run(). A maior parte do trabalho feita, ento, na classe Thread. Abaixo fornecido o cdigo de uma seo da classe Thread.

public class Thread implements Runnable { ... public void run() { if (target != null) { target.run(); } } ... } Do exemplo acima nota-se que a classe Thread tambm implementa a interface Runnable. O mtodo Thread.run() checa se a classe alvo, i.e., a classe que ser executada como uma thread no igual a null, e ento executa o mtodo run() da classe alvo. Quando isto ocorre, o mtodo run() da classe alvo ser executado como uma thread prpria.

Criao de Threads. Considere o exemplo a seguir:

class ThreadSimples extends Thread { public ThreadSimples(String str) { super(str); } public void run() { for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); try { sleep((int)(Math.random() * 1000)); } catch (InterruptedException e) {} } System.out.println("Terminado!" + getName()); } } class TestaDuasThreads { public static void main (String[] args) { new ThreadSimples("Belo Horizonte").start(); new ThreadSimples("Braslia").start(); } } A primeira classe estende a classe Thread do sistema. O seu primeiro mtodo um construtor que recebe uma string como argumento e passa este argumento ao construtor de sua superclasse Thread, que usa este argumento mais tarde no programa para imprimir o nome da thread. O mtodo

33

run() imprime o nome da thread corrente dez vezes atravs da funo getName() , dormindo um tempo aleatrio entre cada impresso. Ao final, o mtodo run() imprime a palavra Terminado. A segunda classe, denominada TestaDuasThreads, prov um mtodo main() que cria duas threads, uma denominada Belo Horizonte e outra denominada Braslia. O mtodo main() tambm inicializa as duas threads logo aps a sua construo atravs da chamada funo start(). Quando este mtodo executado, uma sada similar seguinte obtida:

0 Belo Horizonte 0 Braslia 1 Braslia 1 Belo Horizonte 2 Belo Horizonte 2 Braslia 3 Braslia 3 Belo Horizonte 4 Belo Horizonte 4 Braslia 5 Belo Horizonte 5 Braslia 6 Braslia 6 Belo Horizonte 7 Belo Horizonte 7 Braslia 8 Braslia 9 Braslia 8 Belo Horizonte Terminado! Braslia 9 Belo Horizonte Terminado! Belo Horizonte interessante notar que a sada de um thread intercalada com a sada da outra thread. Isto ocorre porque as duas threads executam simultaneamente. Os dois mtodos run() executam simultaneamente, mostrando resultados ao mesmo tempo. importante que uma thread durma por algum tempo. Se no, esta consumir todo o tempo de CPU para o processo e no permitir que outros mtodos (outras threads, por exemplo) executem.

Inicializao e Interrupo de Threads. O exemplo a seguir, um pouco mais elaborado, descreve os mecanismos de inicializao e interrupo de uma thread, atravs das funes start() e stop().

import java.applet.*;

import java.awt.*; public class ThreadContador extends Applet implements Runnable { Thread t; int Contador; public void init() { Contador=0; t=new Thread(this); t.start(); } public boolean mouseDown(Event e,int x, int y) { t.stop(); return true; } public void run() { while(true) { Contador++; repaint(); try { t.sleep(10); } catch (InterruptedException e) {} } } public void paint(Graphics g) { g.drawString(Integer.toString(Contador),10,10); System.out.println("Contador= "+Contador); } public void stop() { t.stop(); } } Neste exemplo, a classe ThreadContador comea a contar de 0 mostrando o resultado na sada padro e no console de um browser3. A classe ThreadContador forada a implementar a interface Runnable, pois j estende a classe Applet. Numa applet, a execuo comea pelo mtodo init(). Neste mtodo, a varivel Contador inicializada e uma nova instncia da classe Thread criada. Depois da criao da thread, a mesma deve ser inicializada. Isso feito pela chamada funo

A classe ThreadContador executa como uma applet, pois derivada da classe Applet, e como tal executada no contexto de um browser, como por exemplo o Netscape, que age como o console para a exibio de uma Applet.

35

start(), que ento faz uma chamada funo run() da classe alvo da thread. O mtodo run() um loop infinito, que incrementa a varivel Contador, dorme 10 milissegundos, e envia uma requisio para refrescar o console da applet. Para o programa terminar necessrio que o mtodo run() termine, interrompendo deste modo a thread atual. Isso alcanado pela funo stop(), presente no mtodo mouseDown. Deste modo, este programa termina quando o usurio pressiona o mouse com o cursor na regio da applet.

Suspenso e Retomada de Threads. Uma vez que uma thread interrompida, esta no pode ser reinicializada com o mtodo start(), j que o mtodo stop() ter terminado a execuo da mesma. O mtodo sleep() faz que uma thread durma por um determinador perodo de tempo e ento a execuo retomada quando o tempo limite alcanado. Entretanto, isso no ideal, pois em certas condies uma thread deve ser inicializada quando um certo evento ocorre. Neste caso, o mtodo suspend() permite que uma thread tenha sua execuo suspensa e o mtodo resume() permite thread suspensa executar novamente. A applet seguinte modifica o exemplo anterior usando os mtodos suspend() e resume().

public class ThreadContador2 extends Applet implements Runnable { Thread t; int Contador; boolean suspensa; public boolean mouseDown(Event e,int x, int y) { if(suspensa) t.resume(); else t.suspend(); suspensa = !suspensa; return true; } ... } Neste exemplo, uma varivel booleana usada para determinar o estado da thread. O pressionamento do mouse pelo usurio suspender ou retomar a execuo da thread. A distino de diferentes estados de uma applet importante porque alguns mtodos levantam excees se estes so chamados num estado errado. Por exemplo, se uma thread foi inicializada e interrompida, a execuo do mtodo start() levantar uma exceo IllegalThreadStateException.

Escalonamento de Threads. Java tem um escalonador de threads que monitora todas as threads ativas em todos os programas e decide qual thread deve executar e em qual linha de execuo. Duas caractersticas principais definem uma thread: a sua prioridade e um flag denominado daemon flag. Uma regra bsica do escalonador diz que se somente existem daemom threads rodando, a JVM (mquina virtual Java) terminar. Novas threads herdam a prioridade e o daemon flag da thread que foram criadas. O escalonador determina qual thread deve ser executada analisando a prioridade de cada thread. Aquelas com prioridades maiores sero permitidas executar antes do que threads com prioridades mais baixas. O escalonador pode ser preemptivo ou no preemptivo. Escalonadores preemptivos fornecem um certo time-slice para todas as threads que executam no sistema. O escalonador decide qual thread deve executar e ento resume() esta thread por um determinado perodo de tempo. Quando a thread executa por aquele perodo de tempo, ela suspended() e a prxima thread escalonada resumed(). Escalonadores no-preemptivos decidem qual thread deve executar e ento executam esta thread at o seu fim. A thread tem controle completo do sistema pelo tempo que ela quiser. O mtodo yields() um mecanismo pelo qual uma thread fora o escalonador a executar uma outra thread que porventura esteja esperando. Dependendo do sistema no qual Java esteja executando, o escalonador pode ser preeemptivo ou no preemptivo. A gama de prioridade de uma thread varia de 1 a 10. A prioridade default de uma thread Thread.NORM_PRIORITY, que tem o valor 5. Duas outras variveis estticas so disponveis: Thread.MIN_PRIORITY e Thread.MAX_PRIORITY, que tem valores 1 e 10, respectivamente. O mtodo getPriority() retorna a prioridade de uma thread enquanto que o mtodo setPriority() determina a nova prioridade de uma thread. As threads daemon so denominadas threads de servio que normalmente rodam em baixa prioridade e provem uma mecanismo bsico para um programa quando a atividade da mquina est reduzida. Um exemplo de uma daemon thread que est continuamente rodando o coletor de lixo (garbage collector). Esta thread, fornecida pela JVM, procura por variveis que nunca sero acessadas novamente e liberam seus recursos para o sistema. Uma thread pode ligar o flag daemon passando uma varivel booleana true para o mtodo setDaemon(). Se uma varivel booleana false passada, a thread se tornar uma user thread. Entretanto, isto deve ocorrer antes que esta seja inicializada.

37

Sincronizao de Threads. O mecanismo de Threads, como visto at agora, tem um potencial limitado. A sincronizao, por outro lado, permite um uso mais efetivo e completo do mecanismo de threads. Toda instncia de uma classe em Java tem, potencialmente, um monitor associada a ela. Se a classe no possui funes de sincronizao, entretanto, o monitor associado no efetivamente alocado. Um monitor simplesmente uma chave que serializa o acesso a um objeto de uma classe. A fim de obter acesso a um objeto, uma thread deve primeiramente alocar o monitor. Isto ocorre automaticamente sempre que se entra em um mtodo sincronizado. Um mtodo sincronizado criado atravs da palavra chave synchronized na declarao de um mtodo. Durante a execuo de um mtodo sincronizado, a thread mantm para si o monitor daquele objeto de mtodo, ou para o mtodo de classe, se o mtodo esttico. Se uma thread est executando um mtodo sincronizado, uma outra thread que tente acessar este mtodo ser bloqueada at que a primeira thread libere o monitor, seja pela finalizao da execuo do mtodo ou pelo mtodo wait(). A fim de explicitamente obter acesso a um monitor de objeto, uma thread chama um mtodo sincronizado dentro daquele objeto. Para temporariamente liberar o monitor, a thread chama o mtodo wait(). Dado que uma thread deve ter adquirido o monitor do objeto, a chamada a wait() suportada apenas dentro de mtodos sincronizados. O uso de wait() desta forma permite a uma thread rendezvous com alguma outra thread em um ponto de sincronizao especfico. O exemplo abaixo (problema do produtor e consumidor) fornece maiores detalhes de alguns aspectos de sincronizao:

class Produtor extends Thread { private Caixa caixa; private int numero; public Produtor(Caixa c, int numero) { caixa = c; this.numero = numero; } public void run() { for (int i = 0; i < 10; i++) { caixa.coloca(i); System.out.println("Produtor #" + this.numero + " coloquei: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { }

} } } class Consumidor extends Thread { private Caixa caixa; private int numero; public Consumidor(Caixa c, int numero) { caixa = c; this.numero = numero; } public void run() { int valor = 0; for (int i = 0; i < 10; i++) { valor = caixa.retira(); System.out.println("Consumidor #" + this.number + " retirei: " + valor); } } } Neste exemplo, o objeto compartilhado uma classe Caixa, que dispe de dois mtodos principais: coloca() e retira(). A classe Produtor gera inteiros entre 0 e 9, coloca estes inteiros na classe Caixa, e imprime estes valores. O produtor dorme por um perodo de tempo aleatrio antes que um novo nmero seja produzido. J a classe Consumidor, mais voraz, retira os elementos da classe Caixa to logo estes estejam disponveis. interessante notar que nem o consumidor nem o produtor possuem cdigo associado sincronizao necessria para este problema. Isto feito dentro das funes coloca() e retira() da classe caixa. Suponha, entretanto, que no existisse cdigo de sincronizao dentro da classe caixa. isto levaria a uma das duas sadas abaixo, ambas erradas, dependendo se o consumidor mais rpido que o produtor num determinado perodo de tempo ou vice-versa.

Produtor mais rpido: ... Consumidor #1 retirei: 3 Produtor #1 coloquei: 4 Produtor #1 coloquei: 5 Consumidor #1 retirei: 5 ...

39

Consumidor mais rpido: ... Produtor #1 coloquei: 4 Consumidor #1 retirei: 4 Consumidor #1 retirei: 4 Produtor #1 coloquei: 5 ... Tais fatos so provocados por condies de corrida, na qual o consumidor executa assincronamente em relao ao produtor. Deste modo, a classe caixa deve sincronizar a colocao e retirada do nmero contido nela. O consumidor deve retirar cada nmero armazenado exatamente uma vez. Isso atingido atravs das funes wait() e notify(), como descrito no cdigo abaixo.

class Caixa { private int conteudo; private boolean disponivel = false; public synchronized int retira() { while (disponivel == false) { try { wait(); } catch (InterruptedException e) { } } disponivel = false; notify(); return conteudo; } public synchronized void coloca(int valor) { while (disponivel == true) { try { wait(); } catch (InterruptedException e) { } } conteudo = valor; disponivel = true; notify(); } } A caixa tem duas variveis privadas: a varivel (conteudo), que armazena o nmero produzido e a varivel (disponivel), que determina se o nmero produzido pode ser retirado pelo consumidor. Quando a varivel disponivel verdadeira, o consumidor acabou de colocar um novo

valor na caixa e o consumidor ainda no o retirou. O consumidor apenas pode consumir um novo nmero quando (disponivel = = true). Dado que a classe Caixa tem mtodos sincronizados, Java fornece um nico monitor para cada instncia da classe Caixa (incluindo a classe caixa compartilhada pelas classes Produtor e Consumidor). Sempre que uma thread entra em um mtodo sincronizado, a thread que chamou o mtodo adquire o monitor para aquele objeto do qual o mtodo foi chamado. Outras threads no podero chamar um mtodo sincronizado at que o monitor seja liberado 4. Assim, sempre que o produtor chama o mtodo coloca(), este adquire o monitor da caixa, impedindo, portanto, que o mtodo retira() seja executado pelo consumidor. Da mesma forma, quando o procedimento retira() chamado, o monitor adquirido pelo consumidor impedindo que o mtodo coloca() seja executado pelo produtor. O mtodo notify() acorda o consumidor ou o produtor a tentarem exercer suas funes de consumir ou produzir um nmero. Dependendo da valor da varivel disponvel, um dois executar o mtodo wait(), dando a chance ao outro de executar. A aquisio e liberao de um monitor realiza automaticamente pelo sistema, de maneira atmica. Isto garante que condies de corrida no ocorrem nos nveis de implementao das threads, garantindo integridade dos dados. Concluses e Perspectivas

Um processo sempre uma abstrao de um programa sendo executado. O estado de um processo inclui, entre outras informaes, o contedo de seu espao de endereamento, de seus registradores, incluindo um contador de programa e apontador para pilha, e seu estado em relao ao sistema operacional e ao sistema de arquivo - estado de chamadas de sistema e estados de arquivos abertos. Com o advento de multiprocessadores e paralelismo, a abstrao de processo se tornou confusa. Dentro de um espao de endereamento simples, tem-se vrias threads de controle e assim vrios contadores de programa e apontadores para pilhas e estados de vrias chamadas de sistema realizadas concorrentemente. Esta confuso resultou em diferentes grupos de pesquisa usando diferentes terminologias (algumas vezes at conflitantes) para espao de endereamento e threads de controle. Em Mach, o espao de endereamento com todas as suas threads chamada de tarefa

importante frisar que monitores Java so reentrantes, i.e., a mesma thread que detm o controle de um monitor pode chamar um mtodo sincronizado deste objeto, readquirindo o monitor.

41

(task) e as threads de controle so comumente chamadas de threads. Em Topaz, o espao de endereamento chamado de address space e as threads de controle so chamadas de threads - em Topaz, o nome processo evitado. Em Amoeba, originalmente, um espao de endereamento com suas threads chamado de cluster, enquanto uma thread de controle chamada de task. Isto levou a uma confuso com a terminologia de Mach, de modo que atualmente o espao de endereamento em Amoeba chamado de processo e uma thread de controle, de thread [MUL93].

Uma discusso conduzida no newsgroup comp.os.research questiona a existncia de threads. Alguns projetistas de SO (e.g., aqueles envolvidos no Plan 9 da AT&T e no QNX) argumentam que as threads resolvem os sintomas, mas no o problema (sic). Melhor do que usar threads porque o tempo de chaveamento de contexto alto, uma melhor soluo seria consertar o prprio sistema operacional. Segundo a discusso, isto irnico, pois hoje em dia, at sistemas operacionais de computadores pessoais (PC) suportam multiprogramao com auxlio de uma MMU (Memory Management Unit) e, portanto, a programao tpica hoje baseada em espaos comuns de endereamento compartilhados por threads (ainda que a depurao e o desenvolvimento de cdigos seguros sejam mais difceis). Com um tempo de chaveamento menor, processos pesados podem compartilhar uma rea de memria atravs de um ambiente threaded, sem abrir a caixa de Pandora de problemas que uma memria globalmente compartilhada traz.

Bibliografia de base e Referncias Bibliogrficas

[DRA96] Drake, Donald G. Introduction to JavaThreads - JavaWorld; Abril of 1996 (http://www.javaworld.com). [FLA96] Flanagan, David. Java in a NutShell. OReilly &Associates, Inc., USA, 1996. [FR90] Feitelson, D. & Rudolph, L. Distributed Hierarchical Control for Parallel Processing, Vol.23, No. 5, May 1990, pp. 65-77. [HOA78] Hoare, C.A.R. Communicating Sequential Processes. Communications of the ACM, Vol. 21, No. 8, August 1978, pp. 666-677. [JUN96] Junqueira, Bruno de Almeida. Primeiro Trabalho de Sistema Operacional. DCC-UFMG. Trabalho de Disciplina, 1996. [KIT95] Kitajima, Joo Paulo. Programao Paralela Utilizando Mensagens. XIV JAI, Canela, 1995. [MAN96]McManis, Chuck; Synchronizing threads in Java - JavaWorld, Abril of 1996 (http://www.javaworld.com). [MUL93] Mullender, Sape. Kernel Support for Distributed Systems. In: Distributed Systems, ed. Sape Mullender, Addison-Wesley, ACM Press, 1993. [NWS96] Newsgroup comp.os.research. Frequently Asked Questions, 1996. [PRA95a] Prasad, Shashi. Solaris and Windows NT both support powerful multithreading/multiprocessing to help get the job done faster. Byte, October, 1995. [PRA95b] Prasad, Shashi. To truly reap the rewards of a multiprocessor NT system, you have to use threads. Byte, November, 1995. [SIL94] Silberschatz, A. & Galvin, P. Operating System Concepts. Quarta edio, Addison-Wesley, USA, 1994. [SUN] SUN Microsystems. Java Tutorial (http://www.javasoft.com). [TAN92] Tanenbaum, Andrew. Modern Operating Systems. Prentice-Hall, USA, 1992. [WEI93] Weihl, W. Specifications of Concurrent and Distributed Systems. In: Distributed Systems, ed. Sape Mullender, Addison-Wesley, ACM Press, 1993.

Você também pode gostar