Você está na página 1de 9

Muitos sistemas operacionais modernos agora oferecem recursos para

que um processo contenha múltiplos fluxos de controle, ou


threads.Este capítulo apresenta muitos conceitos associados com os
sistemas de computação com threads múltiplos e aborda o uso de
Java para criar e manipular threads.

5.1 – Visão geral


Um thread, às vezes chamado de processo leve (lightweight
process), é uma unidade básica de utilização de CPU; compreende
um ID de thread, um contador de programa, um conjunto de
registradores e uma pilha. Compartilha com outros thread
pertencentes ao mesmo processo sua seção de código, seção de
dados e outros recursos do sistema operacional, tais como arquivos
abertos e sinais. Um processo tradicional, ou pesado (heavyweight),
tem um único fluxo de controle. Processos com múltiplos threads
podem realizar mais de uma tarefa de cada vez.

Muitos pacotes de software que executam em PCs desktop modernos


têm múltiplos threads. Uma aplicação geralmente é implementada
como um processo separado com vários threads de controle. Um
navegador Web pode fazer um thread exibir imagens ou texto
enquanto outro recupera dados da rede. Um processador de textos
pode ter um thread para exibir gráficos, outro thread para ler
sequências de teclas do usuário e um terceiro thread para efetuar a
verificação ortográfica e gramatical em segundo plano.

Existem também situações em que uma única aplicação pode ser


solicitada a realizar várias tarefas semelhantes. Por exemplo, um
servidor Web aceita pedidos de cliente para páginas Web, imagens,
som e assim por diante. Um servidor Web ocupado pode ter vários
(talvez até centenas) de clientes acessando-o de forma concorrente.
Se o servidor Web executasse como um processo tradicional de único
thread, poderia servir apenas um cliente de cada vez com seu
processo único. O tempo que um cliente teria de esperar para que
seu pedido seja atendido poderia ser enorme. Uma solução é fazer o
servidor executar como um único processo que aceita pedidos.
Quando o servidor recebe um pedido, ele cria um processo separado
para atender a esse pedido. No entanto, em vez de incorrer no custo
de criar um novo processo para atender a cada pedido, pode ser mais
eficiente ter um processo que contenha vários threads para atender
ao mesmo propósito. Essa abordagem utilizaria múltiplos threads em
um processo de servidor Web. O servidor criaria um thread separado
que ouviria os pedidos dos clientes; quando um pedido fosse feito,
em vez de criar outro processo, ele criaria outro thread para atender
ao pedido.

Java tem outros usos para os threads. De um lado, Java não tem
conceito de comportamento assíncrono. Por exemplo, quando ela
tiver de efetuar uma conexão telnet em um servidor, o cliente é
bloqueado até que a conexão seja feita ou ocorra um timeout. Um
timeout é um exemplo de evento assíncrono e Java não tem suporte
direto para eventos assíncronos. Se um programa Java tentar se
conectar a um servidor, ele bloqueará até a conexão ser efetuada.
(Considere o que acontecerá se o servidor estiver fora do ar!) A
solução Java é configurar um thread que tentará fazer a conexão ao
servidor e outro thread que inicialmente ficará suspenso durante um
prazo (por exemplo, 60 segundos) e depois será ativado. Quando
esse thread de temporização for ativado, ele verificará se o thread de
conexão ainda está tentando se conectar com o servidor. Se esse
thread ainda estiver tentando, ele irá interrompê-lo e impedirá que
continue a tentar.

5.2 – Benefícios
Os benefícios da programação com múltiplos threads podem ser
divididos em quatro categorias básicas:

1. Capacidade de resposta: O multithreading de uma aplicação


interativa pode permitir que um programa continue executando
mesmo se a parte dele estiver bloqueada ou executando uma
operação demorada, aumentando, assim, a capacidade de
resposta para o usuário. Por exemplo, um navegador Web com
múltiplos threads ainda poderia permitir a interação do usuário
em um thread enquanto uma imagem está sendo em outro
thread.
2. Compartilhamento de recursos: Por default, os threads
compartilham a memória e os recursos do processo aos quais
pertencem. O benefício do compartilhamento do código permite
que uma aplicação tenha vários threads diferentes de
atividades todos dentro do mesmo espaço de endereçamento.
3. Economia: Alocar memória e recursos para a criação de
processos é caro. Como alternativa, como os threads
compartilham recursos do processo aos quais pertencem, é
mais económico criar e realizar a troca de contexto de threads.
Pode ser difícil avaliar empiricamente a diferença em custo de
criar e manter um processo em vez de um thread, mas em
geral é muito mais demorado criar e gerenciar processos do
que threads. No Solaris, criar um processo é aproximadamente
30 vezes mais lento do que criar um thread, e a troca de
contexto é cinco vezes mais lenta.
4. Utilização de arquiteturas multiprocessador: Os benefícios do
multithreading podem ser muito aumentados em uma
arquitetura multiprocessador, na qual cada thread pode estar
executando em paralelo em um processador diferente. Em uma
arquitetura no processador, a CPU geralmente move-se entre
cada thread de forma tão rápida que existe a ilusão de
paralelismo, mas na verdade apenas um thread está sendo
executado de cada vez.

5.3 – Threads de usuário e de kernel

Nossa discussão até aqui tratou threads de forma genérica. No


entanto, pode ser fornecido suporte a threads ao nível do usuário,
para threads de usuário, ou pelo kernel, para threads de kernel.
Essa distinção será discutida a seguir.

3.1 Threads de usuário

Os threads de usuário são suportados acima do kernel e são


implementados por uma biblioteca de threads no nível do usuário. A
biblioteca fornece suporte a criação, escalonamento e gerência de
threads, sem suporte do kernel. Como o kernel não está a par dos
threads de usuário, todas as atividades de criação e escalonamento
de threads são feitas no espaço de usuário, sem necessidade da
intervenção do kernel. Portanto, os threads de usuário geralmente
são rápidos de criar e gerenciar; no entanto, também apresentam
desvantagens. Por exemplo, se o kernel tiver um único thread,
qualquer thread de usuário realizando uma chamada bloqueante ao
sistema causará o bloqueio de todo o processo, mesmo se houver
outros threads disponíveis para execução na aplicação. As bibliotecas
de threads de usuário incluem Pthreads do POSIX, C-threads do Mach
e threads do Solaris.

5.3.2 Threads de kernel

Os threads de kernel são suportados diretamente pelo sistema


operacional : a criação, o escalonamento e a gerência de threads são
feitos pelo kernel no espaço de kernel. Como a gerência de threads é
feita pelo sistema operacional, os threads de kernel são geralmente
mais lentos para criar e gerenciar do que os threads de usuário. No
entanto, como o kernel está gerenciando os threads, se um thread
realizar uma chamada bloqueante ao sistema, o kernel poderá
escalonar outro thread na aplicação para execução. Alem disso, em
um ambiente multiprocessador, o kernel pode escalonar threads em
diferentes processadores. Windows NT, Solaris e Digital Unix são
sistemas operacionais que suportam threads do kernel.

5.4 – Modelos de Multithreading

Muitos sistemas fornecem suporte a threads de usuário e de kernel,


resultando em diferentes modelos de multithreading. Vamos analisar
três tipos comuns de implementação de threading.

5.4.1 Modelo muitos-para-um

O modelo muitos-para-um (many-to-one-model) mapeia muitos


threads de usuário em um threads de kernel. A gerência de threads é
feita no espaço de usuário, sendo assim eficiente, mas o processo
inteiro será bloqueado se um thread efetuar uma chamada
bloqueante ao sistema. Alem disso, como apenas um thread pode
acessar o kernel de cada vez, não é possível executar múltiplos
threads em multiprocessadores. As bibliotecas de threads de usuário
implementadas nos sistemas operacionais que não suportam os
threads de kernel utilizam o modelo muitos-para-um.

5.4.2 Modelo um-para-um

O modelo um-para-um (one-to-one-model) mapeia cada thread de


usuário em um thread de kernel. Fornece mais concorrência do que o
modelo muitos-para-um, permitindo que outro thread execute
quando um thread efetuar uma chamada bloqueante ao sistema;
também permite de múltiplos threads executem em paralelo em
multiprocessadores. A única desvantagem desse modelo é que criar
um thread de usuário requer criar o thread de kernel correspondente.
Como o custo de criar threads de kernel pode prejudicar o
desempenho de uma aplicação, a maior parte das implementações
desse modelo restringem o numero de threads suportados pelo
sistema. O Windows NT e o OS/2 implementam o modelo um-para-
um.

5.4.3 Modelo Muitos-para-muitos

O modelo muitos-para-muitos (many-to-many model) multiplexa


muitos threads de usuário em um numero menor ou igual de threads
de kernel. O numero de threads de kernel pode ser especifico para
determinada aplicação ou determinada maquina (uma aplicação pode
receber mais threads de kernel em um multiprocessador do que em
um uniprocessador). Enquanto o modelo muitos-para-um permite ao
desenvolvedor criar tantos threads de usuário quantos desejar, a
verdadeira concorrência não é obtida porque apenas um thread pode
ser escalonado pelo kernel de cada vez. O modelo um-para-um
permite maior concorrência, mas o desenvolvedor precisa ter cuidado
para não criar um numero excessivo de threads em uma aplicação (e,
em algumas instancias, pode ser limitado no numero de threads que
podem ser criados). O modelo muitos-para-um não apresenta essas
desvantagens: os desenvolvedores podem criar tantos threads
quantos forem necessários, e os threads de kernel correspondentes
podem executar em paralelo em um multiprocessador. Alem disso,
quando um thread realiza uma chamada bloquante ao sistema, o
kernel pode escalonar outro thread para execução. Solaris, IRIX e
digital UNIX suportam esse modelo.

5.5 – Threads do Solaris 2

O Solaris 2 é uma versão do Unix que ate 1992 so oferecia suporte a


processos tradicionais pesados com um único thread de controle. Foi
transformado em um sistema operacional moderno com suporte a
threads nos níveis de kernel e usuário, multiprocessamento simétrico
(SMP) e escalonamento de tempo real.
O solaris 2 suporta threads de usuário com uma biblioteca
contendo APIs para a criação e a gerência de threads. O Solaris 2
define também um nível intermediário de threads. Entre os threads
de usuário e de kernel estão processos leves (LWP). Cada processo
contem pelo menos um LWP. A biblioteca de threads multiplexa os
threads de usuário no pool de LWPs para o processo, e apenas os
threads de usuário conectados no momento a um LWP realizam o
trabalho. O restante é bloqueado ou fica esperando por um LWP no
qual possa rodar.
Todas as operações no kernel são executadas por threads standart de
kernel. Existe um thread de kernel para cada LWP, e há alguns
threads de kernel que executam tarefas do kernel e não tem LWP
associados (por exemplo, um thread para atender aos pedidos de
disco). Os threads de kernel são os únicos objetos escalonados no
sistema. O Solaris implementa o modelo muitos-para-muitos.
Os threads de usuário podem ser limitados ou ilimitados. Um thread
de usuário limitado é permanentemente associado a um LWP.
Apenas esse thread executa no LWP e mediante solicitação o LWP
pode ficar dedicado a um único processador . Restringir um thread é
útil em situações que exigem um rápido tempo de resposta, tais
como uma aplicação de tempo real. Um thread ilimitado não é
ligado permanentemente a LWP algum. Todos os threads desse tipo
em uma aplicação são multiplexados no pool de LWPs disponíveis
para a aplicação. Os threads são ilimitados por default.
Os threads de kernel sao escalonados pelo escalonador do kernel e
executam na CPU ou CPUs do sistema. Se um thread de kernel
bloquear (como quando espera pela conclusao de uma operaçao de
I/O) , o processador fica livre para executar outro thread de kernel.
Se o thread que bloqueou estava executando em nome de um LWP, o
LWP tambem bloqueia. Em consequencia, o thread de usuario ligado
al LWP naquele momento tambem bloqueia. Se um processo tiver
mais de um LWP, outro podera ser escalonado pelo kernel.

5.6 – Threads de Java

Todos os programas Java consistem em pelo menos um thread de


controle. Mesmo um programa java simples com apenas um método
main() executa como um unico thread na JVM. Alem disso, Java
fornece comandos que permitem ao desenvolvedor criar e manipular
threads de controle adicionais no programa.

5.6.1 Criaçao de threads

Uma forma de criar um thread explicitamente é criar uma nova classe


derivada da classe thread e redefinir o metodo run() da classe
thread.
Um objeto dessa classe derivada executara como um thread de
controle separado na JVM. No entanto, criar um objeto derivado da
classe thread nao cria especificamente o novo thread; em vez disso,
é o metodo start() que realmente cria o novo thread. Chamar o
metodo start() para o novo objeto(1) aloca memoria e inicializa um
novo thread na JVM e (2) chama o metodo run() tornando o thread
passivel de execuçao pela JVM.

5.6.2 Gerencia de threads

Java fornece varias APIs para gerenciar threads, incluindo:


 Suspend(): Suspende a execuçao de um thread que
estiver processando no momento.
 Sleep(): Suspende o thread em execução no momento
durante determinado periodo.
 Resume(): Retorna a execuçao de um thread que tinha
sido suspenso.
 Stop(): Interrompe a execução de um thread; depois que
um thread tiver sido interrompido,ele nao poderá ser
retomado ou iniciado.

Cada um dos diferentes metodos para controlar o estado de um thread


pode ser útil em determinadas situaçoes. Por exemplo, os applets sao
exemplos naturais de multithreading porque normalmente tem graficos,
animações e audio – todos bons candidatos a gerencia como threads
separados. No entanto, pode nao fazer sentido para um applet executar
enquanto ele nao estiver sendo exibido, particularmente se o applet estiver
executando uma tarefa com uso intensivo de CPU. Uma forma de lidar com
essa situação é fazer o applet executar como um thread de controle
separado, suspendendo o thread quando o applet nao estiver sendo exibido
e retomando-o quando o applet for exibido novamente.

Você pode fazer isso observando que o metodo start() de um applet é


chamado quando um applet é exibido pela primeira vez. Se o usuario deixar
a pagina web ou o applet sair da área visivel da tela, o metodo stop() do
applet é chamado. Se o usuario retornar a pagina Web do applet, o metodo
start() é chamado novamente. O metodo destroy() de um applet é chamado
quando o applet é removido do cache do navegador. É possivel evitar que
um applet execute enquanto nao estiver sendo exibido em um navegador
da Web fazendo que o metodo stop() do applet suspenda o thread e
retomando a execução de um thread no metodo start() do applet.

O applet clockapplet exibe a data e a hora em um navegador. Se o applet


estiver sendo exibido pela primeira vez, o metodo start() cria um novo
thread; caso contrario, ele retoma a execução de um thread. O metodo
stop() do applet suspende a execução de um thread. O metodo destroy() do
applet interrompe a execução de um thread. O metodo run() do thread tem
um laço infinito que alterna entre a suspensão por um segundo e a
chamada ao metodo repaint(). O metodo repaint() chama o metodo paint()
que exibe a data na janela do navegador.

Com a versao Java 2, os metodos suspend(), resume() e stop() tornaram-


se obsoletos. Os métodos obsoletos ainda sao implementandos na API
atual; no entanto, seu uso é desencorajado.

5.6.3 Estados de um thread

Um thread Java pode estar em um dos quatro estados possiveis a seguir:

1. Novo: Um Thread está neste estado quando um objeto para o thread


é criado (ou seja, a instrução new()).
2. Executável: Chamar o método start() aloca memória para o novo
thread na JVM e chama o método run() para o objeto thread.Quando
o metodo run() de um thread é chamado, o thread passa do estado
Novo para o estado Executável. Um Thread no estado executavel
pode ser executado pela JVM.Observe que Java não faz distinção
entre um thread que é passivel de execução e um thread que está
sendo executado no momento pela JVM. Um thread em execução
ainda está no estado executavel.
3. Bloqueado: Um thread torna-se bloqueado se executar uma instrução
bloqueante, como ao realizar uma operação de I/O ou se chamar
certos metodos Thread Java, como sleep() ou suspend().
4. Terminando: Um thread passa para o estado terminado quando seu
metodo run() termina ou quando o metodo stop() é chamado.

Não é possivel definir o estado exato de um thread, embora o método


isAlive() retorne um valor booleano que um programa pode usar para
determinar se um thread está ou não no estado terminado.

5.6.4 Threads e a JVM

Além de um programa Java que contém vários threads de controle distintos,


existem muitos threads executando assincronamente para a JVM que
tratam de tarefas ao nível de sistema,tais como gerencia de memoria e
controles graficos. Esses threads incluem um thread de coleta de lixo, que
avalia os objetos na JVM para verificar se ainda estão em uso. Se não
estiverem, ele devolve a memória para o sistema. Dentre os outros threads
está um que trata eventos de temporização, tais como chamadas ao
método sleep(), um que trata eventos dos controles gráficos, tais como
pressionar um botão, e um que atualiza a tela. Assim, uma aplicação Java
típica contém vários threads de controle diferentes na JVM. Certos threads
são criados explicitamente pelo programa; outros threads são tarefas de
nivel de sistema executando em nome da JVM.

5.6.5 A JVM e o sitema operacional host

A implementação típica da JVM normalmente é feita sobre um sistema


operacional host. Essa configuração permite que a JVM oculte os detalhes
da implementação do sistema operacional subjacente e forneça um
ambiente consistente e abstrato que permite aos programas Java operar em
qualquer plataforma que suporte uma JVM. A especificação da JVM não
indica como os threads Java serão mapeados no sistema operacional
subjacente, deixando essa decisão para cada implementação particular da
JVM. Em geral, um thread Java é considerado u m thread de usuário, e a
JVM é responsável pela gerência de threads. O Windows NT utiliza o
modelo-um-para-um; portanto, cada thread Java para uma JVM executando
no NT é mapeado em um thread de kernel. O Solaris 2 inicialmente
implementou a JVM usando o modelo muitos-para-um (chamado threads
verdes pela Sun). No entanto, já na Versão 1.1 da JVM como o Solaris 2.6,
a JVM foi implementada usando o modelo muitos-para-muitos.

Você também pode gostar