MULTITAREFA, THREADS, SEMÁFOROS
Email:
Walter Chagas
Atualização: 05/05/2003
1) Definiçáo de Processos
É o ambiente onde se executa um programa. O
processo é quem define o ambiente, os recursos,
e os Buffers disponíveis à este. Nenhum programa
é executado diretamente na memoria e sim dentro
de um processo. Se não fosse isto, o programa
poderia fazer uso indiscriminado de qualquer área
de memória inclusive areas protegidas ou então
efetuando operações de I/O indiscriminadamente em
qualquer área aleatória do disco, provocando o
maior balaio de gato no computador e comprometendo
a integridade e a consistência dos dados. Daí
conclui-se que é através do processo que o sistema
operacional controla a execução, as permissões e
as restrições que o programa terá quando estiver
sendo executado, bem como os recursos que estarão
e quando estarão disponíveis à ele.
1.1) Estados de um processo
Execução (Running):
Quando um processo está sendo processado pela
CPU. Tais processos se revezam na execução.
Pronto (Ready):
Quando um processo aguarda que o sistema
operacional aloque a CPU para sua execução.
Espera (Wait):
Quando um processo está aguardando algum evento
externo para prosseguir com o processamento.
Quando um processo passa a maior parte do temp
no estado de execução, a este chamamos de CPU
BOUND. Este tipo de processo é muito comum em
aplicações matemáticas, científicas e gráficas
por efetuarem muitos cálculos. Quando o processo
passa a maior parte do tempo em estado de espera e
realizando muitas operações de I/O, a este chamamos
de I/O BOUND. É o processo mais comum em aplicações
comerciais.
2) Conceitos Ligados a Processos
2.1) System Calls
É um mecanismo que protege o núcleo do sistema
operacional intermediando as chamadas dos
aplicativos ao nucleo processando as solicitações
e as respostas à estas solicitações. Suas funções
básicas são:
- Gerencia de processos.
- Gerência de memória.
- Gerência de Entrada e Saída..
2.2) Metodos de acesso
É um mecanismo que monitora as instruções
executadas pelos programas de forma que estes
não executem instruções que possam comprometer
a integridade tanto do sistema como dos dados.
As instruções que podem comprometer o sistema
são autorizadas apenas ao Sistema operacional
e este irá intermediar as chamadas à estas
instruções pelos aplicativos.
3) Hierarquia entre Processos
Um processo pode criar outros processos e que
podem, por sua vez, criarem tambem outros
processos de maneira hierárquica. Quando um
processo (Processo pai) cria um outro processo,
a este chamamos de subprocesso ou processo filho,
e este subprocesso poderá criar subprocessos
que podem criar outros subprocessos etc...
gerando uma arvore hierárquica de processos.
Este tipo de recurso evita que o usuario tenha
que esperar que um processo termine para que
sua requisiçao seja processada melhorando o
desempenho do sistema. Se a hierarquia de
processos possúi este benefício, ela
consome recursos deste pois para cada processo
será necessário a alocação dos referidos recursos
de memória, buffers, etc, principalmente nos SO's
atuais que são multitarefa e muitas vezes são
obrigados a rodarem em maquinas domésticas e de
recursos de hardware limitados, de forma que
depois de um certo numero de subprocessos a
situação se torna crítica gerando erros no sistema
como podemos ver abaixo:
Figura 1
Quando voce manda executar um programa dentro
de outro, na verdade voce está criando um
subprocesso que é subordinado ao processo
principal e assim por diante. Imagine a situação
em que o Command.com é executado dentro do processo
principal e os programas, que serão executados no
prompt do DOS, em subprocessos. Neste caso imagine
um programa que é carregado e neste programa
tem uma opção "Ir para o DOS". Quando voce executar
esta opção, será criado mais um subprocesso dentro
deste subprocesso que está sendo executado o
referido programa. Se, dentro deste ambiente do DOS
voce executar algum outro programa voce nada mais
estará fazendo do que criando mais um subprocesso.
Não confunda hierarquia de processos com
multiprocessamento. O ambiente de multiprocessamento
é aquele ambiente onde vários processos podem ser
executados ao mesmo tempo, o chamado "Multitarefa".
4) THREAD's
Para resolver o problema da hierarquia de
processos, foi criado o conceito de "Thread"
onde um processo pode efetuar varias operações
concorrentemente ou simultaneamente através das
chamadas "Linhas de execução". Neste caso, o processo
é dividido no numero de Threads sem que haja
necessidade de ser criado um outro.
Desde que a IBM começou a promover o seu sistema
operacional OS2 2.0 em 1991, ouvimos falar de
"threads" e software "Multi-thread". Mas o que
é este recurso?
Threads compartilham o processador da mesma
maneira que um processo. Por exemplo, enquanto
uma Thread espera por uma operação de I/O, outra
Thread pode ser executada. Cada Thread tem seu
proprio conjunto de registradores mas todas elas
compartilham o mesmo espaço de endereçamento
pois lembre-se que o processo é um só. As Threads
passam pelos mesmos estados de espera que o
processo, ou seja, Running, ready, wait.
Um programa "Multi-thread" pode estar sendo
executado em vários locais ao mesmo tempo. E
qual a vantagem disso? Vamos supor que um
programa deseje imprimir um relatório. Enquanto
o programa estiver enviando os dados para a
impressora, a execução dele estará bloqueada.
O usuário não poderá emitir novos comandos,
inserir um novo registro por exemplo, enquanto
a impressão não terminar. Um programa
"Multi-thread" pode iniciar uma "Thread" para
imprimir enquanto os resto do programa continua
funcionando.
Mas qual a diferença de "Multi-thread" para
Multi Tarefa? A diferença é que o Multi Tarefa
requer que seja iniciado um outro processo caso
se deseje a execução simultânea. Isto aumenta
bastante a complexidade dos aplicativos e traz
uma grande perda de performance.
Para efetuar troca de informações entre duas
Threads de um mesmo programa, basta utilizar
uma variável comum. Entre programas devem ser
utilizados recursos mais sofisticados como
arquivos comuns, filas, etc. As várias threadas
podem ser codificadas em um mesmo fonte. Multiplos
processos exigem a quebra do projeto em vários
executáveis.
Iniciar uma Thread é uma tarefa barata para o
sistema operacional, bem mais simples do que
carregar um outro processo. Além disto, a
troca de execução entre threads de um mesmo
programa é feita com um mínimo de desperdício.
Figura 2
Figura 2 - Exemplo de uma thread, duas seções
de copia de arquivos no Explorer são iniciadas
simultaneamente. Isto somente é possível porque
cadas seção corresponde a uma thread.
Uma interessante exemplo de como usar threads
são as Dll's, procedimento este conhecido como
API. Diversos fornecedores oferecem API's
proprietárias que permitem que outros aplicativos
sejam ampliados de uma maneira mais eficiente.
Utilizando este método, a lógica é implementada
por meio de um conjunto de funções de aplicativos
que são empacotadas como bibliotecas compartilhadas
na forma de DLL (Dynamic Linked Library) no ambiente
Windows ou um SO (Shared Object) no ambiente
UNIX/LINUX e que permitem, às funções do aplicativo,
o acesso direto às estruturas de dados do sistema
Operacional.
Por exemplo, no ambiente Windows, um aplicativo
qualquer chama uma DLL através de um pedido de um
recurso para o qual foi definida uma associação
DLL/função. O sistema operacional aloca este
pedido recebido em uma thread, que ficará em uma
fila de espera. Quando chegar sua vez, será
criada para ela, uma estrutura de dados sobre o
pedido e este então começará a ser executado.
Quando estiver completo, a thread será retornada
ao pool de threads gerenciadas, até que sejam
necessárias novamente.
Normalmente esta técnica tem um desempenho de
até 5 vezes superior ao de um processo comum.
A razão disto é muito simples. A chamada a um
programa é substituída pelo carregamento de um
objeto compartilhado (DLL) que normalmente é
feito somente uma vez. Depois disto, este objeto
permanece dentro do espaço de endereço do sistema
operacional, e as sobrecargas se restringem às
chamadas de função.
À estrutura de dados sobre o pedido, fornecida
pelo sistema operacional para se comunicar com o
objeto compartilhado, recebe o nome de ECB
(EXTENSION CONTROL BLOCK) e consiste em um
conjunto de variáveis, tanto de ambiente como
oriundas do pedido e funções de chamada para
suporte à geração da resposta. Cada pedido tem
seu próprio ECB alocado e estes não se comunicam
entre si em hipótese alguma. Em outras palavras,
um ECB é a forma que o sistema operacional tem de
compartilhar entre as threads, o conjunto de
variáveis ambientes e também de disponibilizar
dados que somente interessarão ao pedido sem que
outro pedido tenha acesso à estes.
Outra grande diferença entre as threads e os
processos é que cada processo tem seu proprio
espaço de endereçamento enquando N threads
compartilham um espaço de endereçamento de um
único processo, otimizando o sistema e consumindo
menos recursos dele, mas ai aparece também a
grande desvantagem deste recurso pois, por causa
desta característica (serem executadas no mesmo
espaço de endereçamento), elas compartilham este
espaço sem nenhuma proteção ou restrição o que
permite que uma Thread possa alterar dados de
uma outra ou vice versa. Com base nesta informação,
voce pode concluir que para que uma aplicação
possa funcionar eficientemente com threads, é
preciso que haja mecanismos de sincronização
robustos e consistentes e que possam ter controle
total sobre a fila de threads que acessarão as
funções e os recursos globais do servidor pois,
um bug ou um erro lógico de programacão, pode
criar um conflito entre duas ou mais Threads
que acabaria detonando todas as outras e
consequentemente derrubar o processo.
Figura 3
Figura 3 - 3 instâncias do Internet Explorer
abertos simultâneamente. Isto somente é possível
porque cada IE está sendo executada em uma thread
separada.
5) Comunicação entre Processos
É comum processos trabalharem concorrendo e
compartilhando recursos do sistema, como
arquivos, registros, dispositivos e áreas de
memória. Na verdade o importante é como estes
processos irão usar e o que irão fazer com
estes recursos. Um recurso mal usado ou usado
indevidamente pode comprometer o sistema ou o
próprio processo gerando falhas como podemos ver
abaixo:
Figura 4
Um dos exemplos é de dois processos concorrentes
que trocam informações através de operação de
gravação e leitura em um Buffer. Um processo só
poderá gravar dados no Buffer caso ele não esteja
cheio, da mesma forma, um processo só poderá ler
dados armazenados no Buffer se existir algum dado
a ser lido.
Para gerenciar este compartilhamento de forma
que dois ou mais processos tenham acesso a um
mesmo recurso compartilhado, existe um mecanismo
que controla este acesso, chamado de MECANISMO DE
SINCRONIZAÇÃO. Este mecanismo tem o propósito de
garantir a confiabilidade e a integridade da
gravação dos dados, evitando que os dados
armazenados fiquem sem consistência. Como exemplo
dois processos efetuando operações de gravação,
de dados diferentes, em disco exatamente no mesmo
setor ou no mesmo arquivo. Esta situação se torna
mais crítica ainda em sistemas operacionais
MULTIPROGRAMÁVEIS.
Este conceito se aplica também aos subprocessos
e as threads.
Na abordagem dos problemas de comunicação
entre processos, são usados algoritmos nos
programas que incorporam o mecanismo de
sincronização e gerenciam este acesso. Um bom
exemplo disto é quando desenvolvemos programas
para trabalharem em rede onde você precisará
fazer o bloqueio de um arquivo para usa-lo
impedindo assim que outra estação de trabalho
manipule aquele mesmo arquivo no momento em
que você estiver usando-o.
Para que sejam evitados problemas desta natureza, onde dois processos manipulem o mesmo arquivo ou
a mesma variável de memória simultaneamente, enquanto um processo estiver acessando determinado
recurso, todos os outros que queiram acessar esse mesmo recurso deverão esperar. Isso se chama
EXCLUSÃO MUTUA.
A exclusão mútua deverá agir apenas sobre os processos que estão concorrendo em um determinado
recurso. Quando desenvolvemos um programa, que faça tratamento de exclusão mutua, este deverá
terá uma seção chamada REGIÃO CRÍTICA. Nesta região existe uma serie de procedimentos e protocolos
que o programa deverá fazer para que o sistema operacional libere o recurso para o mesmo. A região
critica deve ser sempre usada quando seu programa for fazer uso de recursos que são passiveis de
compartilhamento com algum outro suposto programa na memória. É nela também que os processos
encontram-se em um momento mais critico, pois qualquer erro ocorrido ali dentro pode fazer com que
dois ou mais processos colidam gerando falhas e derrubando o sistema . figura abaixo mostra uma
falha ocasionada quando um processo apresentou problemas e acabou interferindo no funcionamento de
outro:
Figura 5
6) Problemas de Sincronização
A tentativa de implementar a exclusão mútua nos programas traz alguns problemas com sincronização.
As mais freqüentes são:
6.1) Velocidade de Execução dos Processos
Um dos problemas causados pela exclusão mutua é quando um processo mais rápido é obrigado à
esperar que um lento use o recurso e o libere. Um gargalo gerado pela consistência dos processos
onde o mais rápido ficará limitado à velocidade do mais lento. Conseqüência disto, o sistema todo
fica lento comprometendo o seu desempenho.
6.2) Starvation
Quem determina as prioridades dos processos é o sistema operacional. Neste caso existem duas
formas do sistema operacional determinar qual será a vez de quem. Ou por escolha aleatória ou por
prioridades. Quando a escolha é aleatória, existirá a probabilidade de um processo nunca ser
escolhido, quando for uma escolha por prioridades, um processo de menor prioridade nunca receberá
o acesso ao recurso, e ai este processo nunca executará sua rotina.
6.3) Sincronização condicional
Quando um recurso não está pronto para ser utilizado, o processo que vai acessar o recurso ficará
em estado de espera até que o mesmo esteja pronto. Existe o risco deste recurso nunca ficar pronto
por já estar com problemas. Ai todo o sistema fica esperando o Recurso resolver sua vida. Um
exemplo disto é o caso do uso de Buffers para leitura e gravação de dados feita pelos processos.
Uma possível falha na memória que impeça o acesso aos buffers e todo o sistema está parado...
7) Soluções de Hardware
Também o hardware traz algumas soluções que
ajudam a diminuir o problema da exclusão
mútua dos processos:
7.1) Desabilitação de interrupções
Faz com que o processo, antes de entrar em sua região crítica desabilite todas as interrupções
externas e a reabilite após deixar a região critica. Este mecanismo também traz seus
inconvenientes. Se um processo entrou na região crítica e desabilitou todas as interrupções ao
sair dela ele deverá HABILITÁ-LAS novamente sob risco de todo o sistema estar comprometido.
7.2) Instrução Test-And-Set
Instrução especial onde um processo apenas lê o conteúdo de uma variável, e armazena seu valor em
outra área podendo neste caso fazer todas as manipulações necessárias e devidas sem precisar de
prioridades ou esperar que a variável original seja liberada.
8) Soluções de Software
Além da exclusão mútua, que soluciona os problemas de compartilhamento de recursos, existem outros
fatores fundamentais para a solução de problemas de sincronização:
- O número de processadores e o tempo de exxecução dos processos.
- Um processo fora de sua regiião crítica nnão pode impedir que outros processos entrem em suas próprias regiões críticas.
- Um processo não pode permaneecer indefiniidamente esperando para entrar em sua região crítica.
Todas as soluções que foram apresentadas para contornar estes inconvenientes apresentavam
problemas da ESPERA OCUPADA, Na espera ocupada, todas vezes que um processo tenta entrar em sua
região crítica ele são impedidas por já existir um outro processo usando o recurso, fazendo o
sistema ficar parado esperando que o mesmo tenha acesso a este respectivo recurso.
8.1) Semáforos
O semáforo é uma variável que fica associada a um recurso compartilhado, indicando quando este
está sendo acessado por um outro processo. Ela terá seu valor alterado quando o processo entra e
quando sai da região crítica de forma que se um outro processo entrar em sua região critica ele
possa checar antes este valor para saber se o recurso esta ou não disponível. Quando o processo
tiver seu acesso impedido, ele será colocado em uma fila de espera associada ao semáforo
aguardando sua vez de utilizar o recurso. Todos os processos da fila terão acesso ao recurso na
ordem de chegada. O semáforo pode ser usado também para implementar sincronizações condicionais.
Isto consiste em um processo que necessita ser notificado sobre a ocorrência de um evento. Pode-se
usar o semáforo para notificar este processo sobre a ocorrência deste evento.
Outro tipo de semáforo usado é SEMÁFORO CONSUMIDOR onde ele pode informar ao processo se o buffer
está cheio ou está vazio.
SEMÁFORO CONTADOR é aquele que notifica os processos sobre o uso dos recursos. Sempre que um
processo usa um recurso qualquer, este semáforo é incrementado sempre que um processo liberar um
recurso ele será decrementado. Este semáforo é útil para evitar que um processo na região crítica
sem que hajam recursos disponíveis no sistema.
O uso de semáforos exige do programador muito cuidado, pois qualquer engano pode gerar bugs em seu
programa que o levem a falhas de sincronização ocasionando quedas e travamento geral do sistema.
8.2) Monitores
São mecanismos de sincronização compostos de um conjunto de procedimentos, variáveis e estrutura
de dados definidos dentro de um módulo cuja finalidade é a implementação automática da exclusão
mútua entre seus procedimentos. Somente um processo pode estar executando um dos procedimentos do
monitor em um determinado instante. Toda vez que um processo chamar um destes procedimentos, o
monitor verifica se já existe outro processo executando algum procedimento do monitor. Caso
exista, o processo fica aguardando a sua vez ate que tenha permissão para executa-lo.
A implementação da exclusão mútua nos monitores é realizada pelo compilador do programa e não mais
pelo programador. Para isto ele irá colocar todas as regiões críticas do programa em forma de
procedimentos no monitor e o compilador se encarregará de garantir a exclusão mútua destes
procedimentos. A comunicação do processo com o monitor passa a ser feita através de chamadas a
seus procedimentos e dos parâmetros passados para eles.
Outra característica do monitor é que os processos, quando não puderem acessar estes procedimentos,
ficarão aguardando em uma fila de espera e enquanto isto, eles poderão executar outros
procedimentos.
Como ele é escrito em uma linguagem de programação, o compilador das outras demais linguagens
deverão ser capazes de reconhecê-la e implementa-la. São raras as linguagens que permitem tal
implementação criando uma limitação para o uso deste recurso.
8.3) Troca de Mensagens
A troca de mensagens é um mecanismo de comunicação e sincronização entre os processos,
implementado pelo sistema operacional através de duas rotinas do sistema SEND e RECEIVE. A rotina
SEND é a responsável pelo envio de uma mensagem para o processo receptor enquanto a rotina RECEIVE
por receber a mensagem do processo transmissor. Tais procedimentos mesmo não sendo mutuamente
exclusivos permitem a comunicação entre os processos e a sincronização entre eles, pois uma
mensagem somente poderá ser lida depois de ter sido enviada e ela somente será envidada após a
ocorrência de um evento.
No sistema de troca de mensagens, existe a possibilidade da mensagem se perder. Para isto foi
implementado o recurso de que o processo receptor ao recebe-la deverá enviar ao processo
transmissor uma mensagem de recebimento. Caso o transmissor não receber esta mensagem em um certo
espaço de tempo ele irá retransmitir esta mensagem.
A comunicação entre processos pode ser feita diretamente. Bastando que o processo que deseja
enviar uma mensagem enderece explicitamente o nome do receptor. Esta característica chama-se
ENDEREÇAMENTO DIRETO e só é permitida à comunicação entre dois processos.
Existe também o ENDEREÇAMENTO INDIRETO que é um mecanismo que consiste no uso de uma área
compartilhada, onde as mensagens podem ser colocadas pelo processo transmissor e retiradas por
qualquer processo.
Existem duas formas de comunicação entre os processos: COMUNICAÇÃO SINCRONA e COMUNICAÇÃO
ASSINCRONA. Uma comunicação é dita Síncrona, quando um processo envia uma mensagem e fica
esperando até que o processo receptor leia a mensagem e mande a notificação de recebimento. Uma
comunicação assíncrona é aquela em que o processo que envia a mensagem não espera notificação de
recebimento.
9) Deadlock
O Deadlock existe em qualquer sistema multiprogramável. Dizemos que um processo está em Deadlock
quando este para de responder porque está esperando por um evento que nunca ocorrerá. Esta
situação é conseqüência do problema da exclusão mútua. Existem as condições onde o Deadlock irá
ocorrer:
- Cada recurso só pode estar aalocado a um único processo em um determinado instante. (Exclusão mútua)
- Um processo além dos recursoos já alocadoos, pode estar esperando por outros recursos.
- Um recurso não pode ser libeerado de um pprocesso porque outros processos desejam o mesmo recurso (Não-preempção)
- Um processo pode ter de espeerar por um rrecurso alocado a outro processo e vice-versa (Espera circular).
9.1) Prevenção de Deadlock
Para prevenir o Deadlock é preciso garantir que uma das quatro condições acima citada nunca ocorra,
dentre as diversas situações já citadas pode ser feito um minucioso trabalho de determinar muito
bem que recursos, quais recursos e quando estes recursos deverão ser disponibilizados aos
processos.
9.2) Detecção de Deadlock
Em sistemas que não possuam mecanismos que previnam a ocorrência de deadlocks, é necessário um
esquema de detecção e correção do problema. A Detecção do Deadlock é um mecanismo que determina a
existência deste e identifica os recursos envolvidos no problema. Um Exemplo deste tipo de
detector é o próprio Gerenciador de tarefas do Windows que detecta o aplicativo que parou de
responder ao sistema causado, possivelmente, por um deadlock, como podemos ver logo abaixo:
Figura 6
9.3) Correção do Deadlock
Geralmente o problema é resolvido eliminando os processos envolvidos e desalojando os recursos
para ele já garantidos. É aquele processo em que você dá um Alt+Cr+Del no Windows e aparece uma
janela informando o aplicativo que não responde. Este aplicativo pode estar em um processo de
Deadlock, neste caso você manda finalizar o aplicativo e tudo voltará ao normal. Muitas vezes este
mecanismo não resolve e pelo contrário gera novos problemas. Se você finalizar um processo que
esteja intimamente envolvido com o sistema operacional ou que esteja usando recursos de baixo
nível do mesmo, você poderá vir a deixa-lo instável ou travado.
Abaixo vemos a caixa de dialogo do Windows que tentará fechar o processo que pode estar parado por
falta de comunicação com o sistema.
Figura 7
O problema do Deadlock é um problema que tende a tornar-se mais critico à medida que os sistemas
operacionais evoluem no sentido de implementar o paralelismo e permitir a alocação dinâmica de um
numero maior de recursos e a execução de um numero maior de processos simultaneamente. Os usuários
sentem muita saudade dos computadores que rodavam o DOS nos bons tempos quando quase não davam
problemas. Mas é bom lembrar que o DOS era um sistema operacional monotarefa e monousuário onde
praticamente tínhamos apenas um único processo rodando de cada vez. Neste caso não existiam os
problemas que um ambiente multitarefa e multiusuário tem hoje. Todos os recursos do sistema
estavam exclusivamente disponíveis para aquele processo e, portanto ele tinha total e plena
liberdade de fazer com estes o que bem entendia. Hoje os sistemas operacionais são mais complexos
rodando em maquinas mais críticas devido à velocidade de processamento tendo um maior numero de
aplicações que rodam simultaneamente e demandando praticamente todos os recursos do sistema ao
mesmo tempo. Muitos destes programas trabalham não só com um, mas com vários processos
simultaneamente o que aumentam as chances de colisões entre eles ou com os recursos do sistema.
|