Conceito: Coincidência
A simultaneidade é a tendência de os fatos acontecerem ao mesmo tempo em um sistema. Quando lidamos com questões de simultaneidade em sistemas de software, há geralmente dois aspectos importantes: sermos capazes de detectar e responder a eventos externos que estão ocorrendo aleatoriamente e assegurarmos que esses eventos serão respondidos no intervalo de tempo mínimo exigido.
Relacionamentos
Descrição Principal
Nota: A simultaneidade é abordada de forma genérica aqui, já que ela pode se aplicar a qualquer sistema. No entanto, ela é particularmente importante em sistemas que precisam reagir a eventos externos em tempo real e que, freqüentemente, têm prazos apertados a serem cumpridos. Para lidar com as demandas específicas desta classe de sistema, o Rational Unified Process (RUP) possui Extensões de Sistema (Reativas) em Tempo Real. Para obter informações adicionais sobre este tópico, consulte Sistemas em Tempo Real.

O que é Simultaneidade?

A simultaneidade é a tendência de os fatos acontecerem ao mesmo tempo em um sistema. A simultaneidade é um fenômeno natural, é claro. No mundo real, em um determinado momento, vários fatos acontecem simultaneamente. Quando projetamos o software para monitorar e controlar sistemas do mundo real, devemos lidar com essa simultaneidade natural.

Quando lidamos com questões de simultaneidade em sistemas de software, há geralmente dois aspectos importantes: sermos capazes de detectar e responder a eventos externos que estão ocorrendo aleatoriamente e assegurarmos que esses eventos serão respondidos no intervalo de tempo mínimo exigido.

Se cada atividade simultânea fosse desenvolvida independentemente, de maneira realmente paralela, isso seria relativamente simples: poderíamos simplesmente criar programas separados para lidar com cada atividade. Os desafios da projeção de sistemas simultâneos surgem, na maioria das vezes, devido às interações que ocorrem entre as atividades simultâneas. Quando as atividades simultâneas interagem, é necessário um pouco de coordenação.

O diagrama é descrito no conteúdo.

Figura 1:  Exemplo de simultaneidade no trabalho: atividades paralelas que não interagem apresentam questões simples de simultaneidade. É quando as atividades paralelas interagem ou compartilham os mesmos recursos que as questões de simultaneidade se tornam importantes.

O tráfego de veículos oferece uma analogia útil. Os streams de tráfego paralelos em rodovias diferentes e com pouca interação trazem poucos problemas. Os streams paralelos em pistas adjacentes requerem um pouco de coordenação para que se obtenha uma interação segura. No entanto, um tipo muito mais rigoroso de interação ocorre em um cruzamento, em que é exigida uma coordenação cuidadosa.

Por que estamos interessados em Simultaneidade?

Quando se fala em simultaneidade, algumas forças propulsoras são externas. Ou seja, elas são impostas pelas demandas do ambiente. Em sistemas do mundo real, vários fatos acontecem simultaneamente e devem ser tratados 'em tempo real' pelo software. Para fazer isso, muitos sistemas de software em tempo real devem ser "reativos". Eles devem responder a eventos gerados externamente que podem ocorrer em momentos aleatórios, em ordem aleatória ou ambos.

Projetar um programa de procedimentos convencional para lidar com essas situações é extremamente complexo. Pode ser muito mais simples dividir o sistema em elementos de software simultâneos para que eles lidem com cada um desses eventos. A expressão-chave aqui é "pode ser", já que a complexidade também é afetada pelo grau de interação entre os eventos.

Podem existir também razões internamente inspiradas para a simultaneidade [LEA97]. A execução paralela de tarefas pode agilizar consideravelmente o trabalho computacional de um sistema se várias CPUs estiverem disponíveis. Até mesmo em um único processador, a multitarefa pode agilizar muito as atividades, impedindo que uma bloqueie a outra enquanto aguarda a E/S, por exemplo. Uma situação comum em que isso ocorre é durante a inicialização de um sistema. Existem geralmente vários componentes e cada um deles requer tempo para que estejam prontos para a operação. A execução seqüencial dessas operações pode ser terrivelmente lenta.

A capacidade de controle do sistema também pode ser aperfeiçoada pela simultaneidade. Por exemplo, uma função pode ser iniciada, parada ou, de outro modo, influenciada no meio do fluxo por outras funções simultâneas - algo extremamente difícil de realizar sem a existência de componentes simultâneos.

O que torna o Software Simultâneo Difícil?

Com todos esses benefícios, por que não utilizamos uma programação simultânea em todas as situações?

A maioria dos computadores e das linguagens de programação é inerentemente seqüencial. Um procedimento ou processador executa uma instrução em um determinado momento. Em um processador seqüencial único, a ilusão de simultaneidade deve ser criada intercalando a execução de diferentes tarefas. As dificuldades nem estão tanto nos mecanismos utilizados para realizar esse procedimento, mas na determinação de quando e como intercalar os segmentos de programa que podem interagir uns com os outros.

Embora seja fácil obter a simultaneidade com vários processadores, as interações tornam-se mais complexas. Em primeiro lugar, existe a questão da comunicação entre as tarefas executadas em diferentes processadores. Geralmente, há diversas camadas de software envolvidas, que aumentam a complexidade e sobrecarregam o tempo de execução. O determinismo é reduzido em sistemas de várias CPUs, já que os clocks e o tempo de execução podem ser diferentes e os componentes podem falhar de modo independente.

Finalmente, os sistemas simultâneos podem ser mais difíceis de compreender pois não possuem um estado de sistema global explícito. O estado de um sistema simultâneo é a agregação dos estados de seus componentes.

Exemplo de um Sistema Simultâneo em Tempo Real: Um Sistema de Elevador

Como exemplo para ilustrar os conceitos que serão abordados, usaremos um sistema de elevador. Mais precisamente, queremos nos referir a um sistema de computador projetado para controlar um grupo de elevadores em um determinado local de um prédio. Obviamente, podem estar ocorrendo vários fatos simultaneamente em um grupo de elevadores - ou nenhum fato! Em um determinado momento, alguém em qualquer andar pode estar solicitando um elevador, enquanto outras solicitações poderão estar pendentes. Alguns elevadores poderão estar parados, ao passo que outros estarão transportando passageiros, respondendo a uma chamada ou ambos. As portas devem abrir e fechar em momentos apropriados. Os passageiros poderão estar obstruindo as portas, pressionando os botões de abertura ou fechamento de portas ou selecionando os andares e, em seguida, mudando de idéia. Os visores precisam ser atualizados, os motores precisam ser controlados, etc., tudo isso sob a supervisão do sistema de controle de elevadores. Em geral, esse é um bom modelo para explorar os conceitos de simultaneidade e para o qual compartilhamos um grau razoavelmente comum de compreensão e um vocabulário funcional.


O diagrama é detalhado no conteúdo.
Figura 2:   Um cenário que envolve dois elevadores e cinco possíveis passageiros distribuídos em 11 andares.

Como os possíveis passageiros solicitam o sistema em diferentes momentos, ele tenta fornecer o melhor serviço global selecionando elevadores que responderão às chamadas com base em seus estados atuais e nos tempos de resposta projetados. Por exemplo, quando o primeiro passageiro, Andy, chama um elevador para descer, os dois estão ociosos e, portanto, o que está mais próximo, o Elevador 2, responde, embora ele deva primeiro subir para chegar até Andy. Por outro lado, poucos momentos depois, quando o segundo passageiro, Bob, solicita um elevador para subir, o Elevador 1, o mais distante, responde, pois se sabe que o Elevador 2 descerá primeiro para um andar ainda não conhecido e somente depois poderá responder a qualquer chamada vinda dos andares de cima.

Simultaneidade como uma Estratégia de Simplificação

Se o sistema de elevador tivesse apenas um elevador que precisasse atender somente a um passageiro por vez, poderíamos nos ver tentados a achar que seria possível manipulá-lo com um programa seqüencial normal. Mesmo nesse caso "simples", o programa exigiria várias ramificações para acomodar as diferentes condições. Por exemplo, se o passageiro nunca entrasse no elevador e selecionasse um andar, deveríamos redefinir o elevador para permitir que ele respondesse a uma outra chamada.

O requisito normal para manipular chamadas de vários passageiros possíveis e as solicitações de vários passageiros exemplifica as forças propulsoras externas da simultaneidade que discutimos anteriormente. Como os possíveis passageiros conduzem suas próprias vidas simultaneamente, eles solicitam o elevador em momentos aparentemente aleatórios, não importando qual seja o estado do elevador. É extremamente difícil projetar um programa seqüencial que possa responder a qualquer um desses eventos externos a qualquer momento e, ao mesmo tempo, continuar conduzindo o elevador de acordo com as decisões passadas.

Abstraindo Simultaneidade

Para projetar sistemas simultâneos efetivamente, devemos ser capazes de raciocinar sobre a função da simultaneidade no sistema e, para isso, precisamos de abstrações da própria simultaneidade.

Os blocos de construção fundamentais dos sistemas simultâneos são as "atividades", que agem de forma mais ou menos independente umas das outras. Uma abstração gráfica útil para refletir sobre essas atividades são os "encadeamentos de tempo" de Buhr. [BUH96] Nosso cenário de elevador na Figura 3 utilizou, na verdade, uma forma deles. Cada atividade é representada como uma linha pela qual a atividade viaja. Os pontos grandes representam o local em que uma atividade inicia ou aguarda um evento antes de continuar. Uma atividade pode disparar outra para continuar, o que é representado na notação de thread de tempo tocando o local de espera do outro thread de tempo.

O diagrama é descrito no conteúdo.

Figura 3:   Uma visualização dos encadeamentos de execução

Os blocos de construção básicos do software são os procedimentos e as estruturas de dados, mas eles sozinhos são inadequados para uma reflexão sobre simultaneidade. Quando o processador executa um procedimento, ele segue um caminho específico, dependendo das condições atuais. Esse caminho pode ser chamado de "encadeamento de execução" ou "encadeamento de controle". Esse thread de controle pode assumir diferentes ramificações ou loops, dependendo das condições existentes no momento e, em sistemas de tempo real, ele pode ficar parado por um período específico ou aguardar um tempo programado para retomar a execução.

Do ponto de vista do designer do programa, o thread de execução é controlado pela lógica do programa e programado pelo sistema operacional. Quando o designer do software opta por ter um procedimento chamando outros, o thread de execução salta de um procedimento para outro, retornando para continuar de onde ele parou quando uma instrução de retorno é encontrada.

Do ponto de vista da CPU, há somente um thread principal de execução que percorre todo o software, complementado por curtos threads separados que são executados em resposta às interrupções de hardware. Como tudo é criado nesse modelo, é importante que os designers o conheçam. Os designers de sistemas em tempo real, muito mais que os designers de outros tipos de software, devem compreender detalhadamente como um sistema funciona. Esse modelo, no entanto, está em um nível tão inferior de abstração que só pode representar uma granularidade de simultaneidade muito comum, a da CPU. Para projetar sistemas complexos, será muito útil conseguir trabalhar em vários níveis de abstração. A abstração é, obviamente, a criação de uma visão ou de um modelo que suprime os detalhes desnecessários, a fim de que possamos enfocar perfeitamente o que é importante para o problema.

Para avançarmos um nível, normalmente pensamos no software em termos de camadas. No nível mais básico, o Sistema Operacional (SO) é disposto em camadas entre o hardware e o software aplicativo. Ele fornece ao aplicativo serviços baseados em hardware, como a memória, a temporização e a E/S, mas abstrai a CPU para criar uma máquina virtual que seja independente da configuração real de hardware.

Realizando Simultaneidade: Mecanismos

Gerenciando Encadeamentos de Controle

Para oferecer suporte à simultaneidade, um sistema deve fornecer vários threads de controle. A abstração de um thread de controle pode ser implementada de várias maneiras pelo hardware e pelo software. Os mecanismos mais comuns são variações de um destes itens [DEI84], [TAN86]:

  • Multiprocessamento - várias CPUs executando simultaneamente
  • Multitarefa - os sistemas operacionais simulam simultaneidade em uma única CPU
    intercalando a execução de diferentes tarefas
  • Soluções baseadas em aplicativos - o software aplicativo assume a responsabilidade de
    alternar entre as diferentes ramificações de código em tempos apropriados

Multitarefa

Quando o sistema operacional é multitarefa, uma unidade comum de simultaneidade é o processo. Um processo é uma entidade fornecida, suportada e gerenciada pelo sistema operacional cuja finalidade única é oferecer um ambiente em que um programa possa ser executado. O processo fornece um espaço de memória para uso exclusivo do programa aplicativo, um thread de execução para executá-lo e, talvez, alguns meios para enviar e receber mensagens de outros processos. Na prática, o processo é uma CPU virtual para execução de uma parte simultânea de um aplicativo.

Cada processo tem três estados possíveis:

  • bloqueado - aguardando para receber alguma entrada ou o controle sobre algum recurso;
  • pronto - aguardando o sistema operacional conceder a ele a vez para que inicie a execução;
  • em execução - utilizando realmente a CPU.

Os processos geralmente recebem prioridades relativas. O kernel do sistema operacional determina qual processo será executado em um determinado momento com base em seus estados, em suas prioridades e em uma política de programação. Na verdade, os sistemas operacionais multitarefas compartilham um único thread de controle entre todos os seus processos.

Nota: Os termos 'tarefa' e 'processo' são geralmente utilizados de modo intercambiável. Infelizmente, o termo 'multitarefa' é geralmente usado para exprimir a capacidade de gerenciar vários processos de uma só vez, enquanto 'multiprocessamento' se refere a um sistema com vários processadores (CPUs). Adotamos essa convenção porque é a mais comumente aceita. No entanto, utilizamos o termo 'tarefa' com moderação e, quando isso acontece, é para fazer uma pequena distinção entre a unidade de trabalho que está sendo executada (a tarefa) e a entidade que fornece os recursos e o ambiente para ela (o processo).

Dissemos antes que, do ponto de vista da CPU, existe somente um thread de execução. Assim como um programa aplicativo pode saltar de um procedimento para outro chamando sub-rotinas, o sistema operacional pode transferir o controle de um processo para outro, caso ocorra uma interrupção, um procedimento seja concluído ou ocorra qualquer outro evento. Devido à proteção de memória oferecida por um processo, essa "alternância entre tarefas" pode acarretar uma sobrecarga considerável. Além do mais, como a política de programação e os estados de processo têm pouco a oferecer do ponto de vista do aplicativo, a intercalação de processos é geralmente um nível muito inferior de abstração para refletirmos sobre o tipo de simultaneidade que é importante para o aplicativo.

Para refletirmos claramente sobre a simultaneidade, é importante fazer a distinção entre o conceito de um thread de execução e o da alternância entre tarefas. Cada processo pode ser considerado como mantenedor de seu próprio thread de execução. Quando o sistema operacional alterna entre processos, um thread de execução é temporariamente interrompido e o outro inicia ou retoma de onde o primeiro parou.

Multiencadeamento

Muitos sistemas operacionais, particularmente aqueles utilizados para aplicativos em tempo real, oferecem uma alternativa "mais leve" aos processos: os "encadeamentos" ou "encadeamentos simples"

Os threads são uma forma de obter uma granularidade levemente mais fina de simultaneidade em um processo. Cada thread pertence a um único processo e todos os threads de um processo compartilham um espaço de memória único e os outros recursos controlados por esse processo.

Geralmente, cada thread recebe um procedimento para executar.

Nota: É lamentável que o termo 'encadeamentos' esteja sobrecarregado. Quando usamos a palavra 'thread' propriamente, como fazemos aqui, estamos nos referindo a um 'thread físico' fornecido e gerenciado pelo sistema operacional. Quando mencionamos um 'thread de execução', 'thread de controle' ou 'thread de tempo' como na discussão anterior, queremos dizer uma abstração que não é necessariamente associada a um thread físico.

Multiprocessamento

Obviamente, a existência de vários processadores oferece a oportunidade de uma execução verdadeiramente simultânea. Muito freqüentemente, cada tarefa é atribuída de modo permanente a um processo em um processador específico, mas, em algumas circunstâncias, as tarefas podem ser atribuídas de forma dinâmica ao próximo processador disponível. Talvez, a maneira mais acessível de fazer isso seja utilizando um "multiprocessador simétrico". Em uma configuração de hardware desse tipo, várias CPUs podem acessar a memória por meio de um barramento comum.

Os sistemas operacionais que oferecem suporte a multiprocessadores simétricos podem, de forma dinâmica, atribuir threads a qualquer CPU disponível. Exemplos de sistemas operacionais que oferecem suporte a multiprocessadores simétricos são o Solaris da SUN e o Windows NT da Microsoft.

Problemas Fundamentais de Software Simultâneo

Anteriormente, fizemos as afirmações aparentemente paradoxais de que a simultaneidade tanto aumenta como diminui a complexidade do software. O software simultâneo oferece soluções mais simples para problemas complexos, basicamente porque ele permite uma "separação de interesses" entre atividades simultâneas. Nesse sentido, a simultaneidade é apenas mais uma ferramenta que aumenta a modularidade do software. Quando um sistema precisa executar atividades ou responder a eventos predominantemente independentes, atribuí-los a componentes simultâneos individuais naturalmente simplifica o design.

As complexidades adicionais associadas ao software simultâneo são quase que inteiramente provocadas por situações em que essas atividades simultâneas são quase, mas não totalmente, independentes. O seja, as complexidades surgem de suas interações.  Do ponto de vista prático, as interações entre atividades assíncronas envolvem invariavelmente a troca de algumas formas de sinais ou informações. As interações entre threads simultâneos de controle trazem à tona um conjunto de questões que são exclusivas dos sistemas simultâneos e que devem ser tratadas para garantir que um sistema se comportará corretamente.

Interação Assíncrona x Síncrona

Embora haja várias realizações específicas da IPC (comunicação entre processos) ou dos mecanismos de comunicação entre threads, elas podem ser finalmente classificadas em duas categorias:

Em comunicação assíncrona, a atividade de envio redireciona suas informações independentemente de o receptor estar ou não pronto para recebê-las. Após o envio das informações para seu destino, o emissor continuará realizando suas tarefas normais. Se o receptor não estiver pronto para receber as informações, estas serão colocadas em alguma fila onde o receptor possa recuperá-las posteriormente. O emissor e o receptor funcionam de modo assíncrono reciprocamente e, conseqüentemente, não podem fazer suposições sobre o estado um do outro. A comunicação assíncrona é geralmente chamada de transmissão de mensagens.

A comunicação síncrona inclui a sincronização entre o emissor e o receptor, além da troca de informações. Durante a troca de informações, as duas atividades simultâneas se fundem executando, na verdade, um segmento compartilhado de código. Depois, quando a comunicação é concluída, elas se dividem novamente. Portanto, durante esse intervalo, elas ficam sincronizadas e livres de quaisquer conflitos entre elas. Se uma atividade (emissor ou receptor) estiver pronta para se comunicar antes da outra, ela ficará suspensa até que a outra fique pronta também. Por esse motivo, este modo de comunicação é, algumas vezes, chamado de rendezvous.

Um possível problema da comunicação síncrona é que, enquanto uma atividade aguarda a disponibilidade de seu par, ela não é capaz de reagir a nenhum outro evento. Em vários sistemas em tempo real, isso nem sempre é aceitável, pois talvez não seja possível garantir que uma resposta a uma situação importante chegará a tempo. Uma outra desvantagem é que esse tipo de comunicação está propenso a conflito. Um conflito ocorre quando duas ou mais atividades estão envolvidas em um círculo vicioso de espera uma da outra.

Quando existe a necessidade das interações entre atividades simultâneas, o designer deve optar entre um estilo síncrono ou assíncrono. Por síncrono, queremos dizer que dois ou mais threads simultâneos de controle devem se encontrar em um momento específico. Isso geralmente significa que um thread de controle deve aguardar outro para responder a uma solicitação. A forma mais simples e mais comum de interação síncrona ocorre quando a atividade simultânea A requer informações da atividade simultânea B para prosseguir com seu próprio trabalho.

As interações síncronas são, é claro, a regra para os componentes de software não simultâneos. As chamadas de procedimento comuns são um ótimo exemplo de interação síncrona: quando um procedimento chama outro, o responsável pela chamada transfere imediatamente o controle para o procedimento chamado e efetivamente "aguarda" o controle voltar para ele. No mundo da simultaneidade, no entanto, é necessário ter mecanismos adicionais para sincronizar threads de controle independentes.

As interações assíncronas não requerem um rendezvous de tempo, mas ainda requerem um mecanismo adicional para oferecer suporte à comunicação entre dois threads de controle. Geralmente, esse mecanismo assume a forma de canais de comunicação com filas de mensagens, a fim de que as mensagens possam ser enviadas e recebidas de modo assíncrono.

Observe que um único aplicativo pode combinar a comunicação síncrona e a comunicação assíncrona, e usará uma ou outra dependendo do seguinte: se é necessário aguardar uma resposta ou se ele tem outro trabalho que possa realizar enquanto o receptor da mensagem está processando a mensagem.

Tenha em mente que a simultaneidade verdadeira dos processos ou encadeamentos somente é possível em multiprocessadores com execução simultânea de processos ou encadeamentos. Em um processador único, a ilusão da execução simultânea de encadeamentos ou processos é criada pelo programador do sistema operacional, que fragmenta os recursos de processamento disponíveis em pequenas partes para dar a impressão de que há vários encadeamentos ou processos sendo executados simultaneamente. Um design simples anulará essa fatia de tempo, criando vários processos ou encadeamentos que se comunicarão de modo freqüente e síncrono, fazendo com que eles despendam grande parte da sua "fatia de tempo" efetivamente bloqueada e aguardando uma resposta de outro processo ou encadeamento.

Contenção para Recursos Compartilhados

As atividades simultâneas podem depender de recursos escassos que devem ser compartilhados entre elas. Exemplos comuns são os dispositivos de E/S. Se uma atividade exigir um recurso que esteja sendo usado por outra atividade, ela deve aguardar seu retorno.

Condições de Disputa: O Problema de Estado Consistente

Talvez, a questão mais importante do design do sistema simultâneo seja evitar "condições de competição". Quando parte de um sistema precisa executar funções que dependam de um estado (ou seja, funções cujos resultados dependam do estado atual do sistema), ele deve ter a certeza de que o estado se manterá consistente durante a operação. Em outras palavras, determinadas operações devem ser "atômicas". Sempre que dois ou mais encadeamentos de controle tiverem acesso às mesmas informações de estado, será necessária uma forma de "controle de simultaneidade", a fim de assegurar que um encadeamento não modifique o estado enquanto o outro estiver executando uma operação atômica dependente de estado. As tentativas simultâneas de acesso às mesmas informações de estado que podem tornar o estado internamente inconsistente são denominadas "condições de competição".

Um exemplo comum de condição de competição poderia facilmente ocorrer no sistema de elevador quando um andar fosse selecionado por um passageiro. Nosso elevador funciona com listas dos andares que serão visitados ao percorrer cada direção, para cima e para baixo. Sempre que o elevador chega a um andar, um thread de controle remove esse andar da lista apropriada e obtém o próximo destino da lista. Se a lista estiver vazia, o elevador mudará de direção, caso a outra lista contenha andares, ou ficará ocioso, se as duas listas estiverem vazias. Um outro thread de controle é responsável por inserir as solicitações de andar na lista apropriada quando os passageiros escolhem seus andares. Cada encadeamento está executando combinações de operações na lista que não são inerentemente atômicas: por exemplo, verificando o próximo slot disponível e ocupando-o. Se os threads intercalarem suas operações, eles poderão sobrescrever facilmente o mesmo slot na lista.

Conflitos

O conflito é uma condição em que dois threads de controle ficam bloqueados: um aguardando o outro executar alguma ação. Ironicamente, o conflito ocorre, em geral, porque aplicamos algum mecanismo de sincronização para evitar condições de competição.

O exemplo do elevador como uma condição de competição poderia facilmente ocasionar um caso relativamente benigno de conflito. O thread de controle do elevador pensa que a lista está vazia e, assim, nunca visita um outro andar. O thread de solicitação de andar pensa que o elevador está trabalhando para esvaziar a lista e que, portanto, não é necessário instruir o elevador a sair do estado ocioso.

Outros Problemas Práticos

Além das questões "fundamentais", existem algumas questões práticas que devem ser tratadas explicitamente no design do software simultâneo.

Vantagens e Desvantagens de Desempenho

Em uma única CPU, os mecanismos necessários para simular a simultaneidade por meio da alternância entre tarefas usam ciclos de CPU que podem ser utilizados no próprio aplicativo. Por outro lado, se o software precisar aguardar os dispositivos de E/S, por exemplo, as melhorias de desempenho proporcionadas pela simultaneidade podem ser muito mais importantes que qualquer sobrecarga acrescentada.

Vantagens e Desvantagens de Complexidade

O software simultâneo requer mecanismos de coordenação e controle que não são necessários aos aplicativos de programação seqüenciais. Esses mecanismos tornam o software simultâneo mais complexo e aumentam as oportunidades de erros. Os problemas nos sistemas simultâneos também são inerentemente mais difíceis de diagnosticar devido aos vários threads de controle. Por outro lado, como já dissemos antes, quando as forças propulsoras externas são simultâneas, o software simultâneo que manipula os vários eventos de forma independente pode ser muito mais simples do que um programa seqüencial que precise acomodar os eventos em ordem arbitrária.

Não-determinismo

Como diversos fatores determinam a intercalação da execução dos componentes simultâneos, o mesmo software pode responder à mesma seqüência de eventos em uma ordem diferente. Dependendo do design, essas mudanças na ordem podem produzir resultados diferentes.

A Função do Software de Aplicativo no Controle de Simultaneidade

O software aplicativo pode ou não estar envolvido na implementação do controle da simultaneidade. Existe todo um espectro de possibilidades, incluindo as seguintes, por ordem de envolvimento crescente:

  1. As tarefas de aplicativo podem ser interrompidas a qualquer momento pelo sistema operacional (multitarefa preemptiva).
  2. As tarefas de aplicativo podem definir unidades de processamento atômicas (seções críticas) que não devem ser interrompidas e informar ao sistema operacional quando elas forem ativadas e desativadas.
  3. As tarefas de aplicativo podem decidir quando renunciarão ao controle da CPU em outras tarefas (multitarefa cooperativa).
  4. O software aplicativo pode assumir total responsabilidade pela programação e controle da execução de várias tarefas.

Essas possibilidades não são um conjunto exaustivo nem são mutuamente exclusivas. Em um dado sistema, uma combinação dessas possibilidades pode ser empregada.

Abstraindo Simultaneidade

Um erro comum no design do sistema simultâneo é selecionar os mecanismos específicos a serem usados para simultaneidade logo no início do processo de design. Cada mecanismo oferece vantagens e desvantagens. A seleção do "melhor" mecanismo para uma situação específica é geralmente determinada pelas sutis compensações e compromissos. Quanto mais cedo um mecanismo for escolhido, haverá menos informações para basear a seleção. A escolha do mecanismo também tende a reduzir a flexibilidade e a capacidade de adaptação do design nas diferentes situações.

Assim como acontece com as tarefas de design mais complexas, a simultaneidade é melhor compreendida usando vários níveis de abstração. Primeiro, os requisitos funcionais do sistema devem ser bem compreendidos no que diz respeito ao comportamento desejado. Depois, os possíveis papéis da simultaneidade devem ser explorados. A melhor maneira de fazer isso é utilizando a abstração de threads sem se comprometer com uma implementação específica. A seleção final dos mecanismos de simultaneidade deve permanecer, o máximo possível, em aberto, a fim de permitir o ajuste fino do desempenho e a flexibilidade para distribuir os componentes de modo diferente nas várias configurações do produto.

A "distância conceitual" entre o domínio do problema (por exemplo, um sistema de elevador) e o domínio da solução (construções de software) permanece como uma das maiores dificuldades do design do sistema. Os "formalismos visuais" são extremamente úteis para compreender e comunicar idéias complexas, como o comportamento simultâneo e, na prática, transpor essa brecha conceitual. Entre as ferramentas que comprovaram ser valiosas na solução desses problemas estão:

  • os diagramas de módulos que prevêem os componentes que estão atuando simultaneamente;
  • os threads de tempo que prevêem atividades simultâneas e interativas (que podem ser ortogonais aos componentes);
  • os diagramas de seqüências que visualizam interações entre componentes;
  • os fluxogramas de transição de estado que definem os estados e os comportamentos dependentes de estado dos componentes.

Objetos como Componentes Simultâneos

Para projetar um sistema de software simultâneo, devemos combinar os tijolos de construção do software (procedimentos e estruturas de dados) com os tijolos de construção da simultaneidade (threads de controle). Discutimos o conceito de uma atividade simultânea, mas não é possível construir sistemas das atividades. É possível construir sistemas dos componentes; faz sentido construir sistemas simultâneos dos componentes simultâneos. Nenhum procedimento, estrutura de dados ou thread de controle cria, por si mesmo, modelos muito naturais para componentes simultâneos, mas os objetos parecem ser uma forma muito natural de combinar todos esses elementos necessários em um único pacote simples.

Um objeto empacota os procedimentos e as estruturas de dados em um componente coeso com seu próprio estado e comportamento. Ele encapsula a implementação específica desse estado e comportamento, e define uma interface por meio da qual os outros objetos ou software poderão interagir com ele. Os objetos geralmente modelam entidades ou conceitos do mundo real e interagem com outros objetos por meio da troca de mensagens. Agora, eles são bem aceitos por muitas pessoas como a melhor maneira de construir sistemas complexos.

O diagrama é descrito no conteúdo.

Figura 4:   Um conjunto simples de objetos para o sistema de elevador.


Considere um modelo de objeto para o sistema de elevador. Um objeto de estação de chamada em cada andar monitora os botões de chamada para cima e para baixo nesse andar. Quando um provável passageiro pressiona um botão, o objeto estação de chamada responde enviando uma mensagem a um objeto despachante de elevador, que, por sua vez, seleciona o elevador com maior probabilidade de fornecer o serviço mais rápido, despacha o elevador e reconhece a chamada. Cada objeto elevador controla simultânea e independentemente seu parceiro físico (o outro elevador), respondendo às seleções de andar do passageiro e às chamadas do despachante.

A simultaneidade pode assumir duas formas em um modelo de objeto. A simultaneidade entre objetos ocorre quando dois ou mais objetos estão executando atividades independentemente por meio de threads de controle separados. A simultaneidade intra-objeto surge quando vários threads de controle estão ativos em um único objeto. Na maioria das linguagens orientadas a objetos de hoje, os objetos são "passivos" e, portanto, não têm seu próprio encadeamento de controle. Os threads de controle devem ser fornecidos por um ambiente externo. Muito freqüentemente, o ambiente é um processo padrão de S.O. criado para executar um "programa" orientado a objetos escrito em uma linguagem como C++ ou Smalltalk. Se o SO oferecer suporte ao mecanismo multithreading, vários threads poderão ficar ativos no mesmo ou em vários objetos.

Na figura a seguir, os objetos passivos são representados pelos elementos circulares. A área interna sombreada de cada objeto é sua informação de estado e o anel externo segmentado é o conjunto de procedimentos (métodos) que definem o comportamento do objeto.

O diagrama é detalhado no conteúdo.
Figura 5:   Ilustração de interação do objeto.

A simultaneidade intra-objeto traz consigo todos os desafios do software simultâneo, como a possibilidade de condições de competição quando vários encadeamento de controle tiverem acesso ao mesmo espaço de memória - nesse caso, os dados encapsulados no objeto. Talvez, alguém tenha pensado que o encapsulamento de dados traria uma solução para essa questão. O problema, obviamente, é que o objeto não encapsula o thread de controle. Embora a simultaneidade entre objetos evite essas questões na maioria das vezes, há ainda um problema inquietante. Para que dois objetos simultâneos interajam trocando mensagens, pelo menos dois threads de controle devem manipular a mensagem e acessar o mesmo espaço de memória para enviá-la. Um problema relacionado (no entanto, ainda mais difícil) é o da distribuição de objetos entre diferentes processos ou, até mesmo, entre processadores. As mensagens entre objetos de diferentes processos requerem suporte à comunicação entre processos e, geralmente, exigem que a mensagem seja codificada e decodificada em dados que possam ser passados para além das fronteiras do processo.

Nenhum desses problemas é intransponível, é claro. Na verdade, como dissemos na seção anterior, todos os sistemas simultâneos devem lidar com esses problemas; portanto, há soluções comprovadas. O problema é que o "controle da simultaneidade" gera trabalho extra e introduz oportunidades de erro adicionais. Além do mais, ele obscurece a essência do problema do aplicativo. Por todos esses motivos, queremos minimizar a necessidade de os programadores de aplicativo lidarem explicitamente com eles. Uma maneira de conseguir isso é criar um ambiente orientado a objetos com suporte à transmissão de mensagens entre objetos simultâneos (incluindo o controle de simultaneidade) e minimizar ou eliminar o uso de vários threads de controle em um único objeto. Na verdade, essa medida encapsula o thread de controle juntamente com os dados.

O Modelo de Objeto Ativo

Os objetos que têm seus próprios encadeamentos de controle são chamados "objetos ativos". Para oferecer suporte à comunicação assíncrona com outros objetos ativos, cada objeto ativo é fornecido com uma fila de mensagens ou "caixa postal". Quando um objeto é criado, o ambiente fornece a ele seu próprio thread de controle, que o objeto encapsula até o fim. Assim como acontece com um objeto passivo, o objeto ativo fica inativo até que chegue uma mensagem de fora. O objeto executa o código apropriado para processar a mensagem. Qualquer mensagem que chegue enquanto o objeto estiver ocupado será enfileirada na caixa de correio. Quando o objeto concluir o processamento de uma mensagem, ele retornará para resgatar a próxima mensagem que está na caixa de correio ou aguardará a chegada de uma. Boas sugestões a objetos ativos no sistema de elevador incluem os próprios elevadores, as estações de chamada em cada andar e o despachante.

Dependendo de sua implementação, os objetos ativos podem se tornar bastante eficazes. No entanto, eles geram mais sobrecarga do que um objeto passivo. Desse modo, como nem todas as operações precisam ser simultâneas, é comum combinar objetos ativos e passivos no mesmo sistema. Devido aos estilos de comunicação diferentes, é difícil torná-los parceiros. No entanto, um objeto ativo cria um ambiente ideal para os objetos passivos, substituindo os processo de SO que usamos anteriormente. Na verdade, se o objeto ativo delegar todo o trabalho aos objetos passivos, ele será basicamente o equivalente a um processo ou thread de SO com recursos de comunicação entre processos. No entanto, os objetos ativos mais interessantes têm seu próprio comportamento para realizar parte do trabalho, delegando outras partes aos objetos passivos.

O diagrama é descrito no conteúdo.

Figura 6:   Um objeto 'ativo' fornece um ambiente para classes passivas

Boas sugestões a objetos passivos dentro de um objeto de elevador ativo incluem uma lista de andares em que o elevador deverá parar enquanto estiver subindo e uma outra lista que ele usará quando estiver descendo. O elevador deve ser capaz de perguntar à lista qual é a próxima parada, adicionar novas paradas à lista e remover as paradas que já foram atendidas.

Como os sistemas complexos são quase sempre compostos por vários níveis de subsistemas até chegar aos componentes de nível-folha, é natural para o modelo de objeto ativo permitir que os objetos ativos contenham outros objetos ativos.

Embora um objeto ativo de thread único não ofereça suporte à verdadeira simultaneidade intra-objeto, delegar trabalho aos objetos ativos armazenados é uma substituição razoável para vários aplicativos. Ele oferece a importante vantagem do encapsulamento completo do estado, do comportamento e do thread de controle por objeto, o que simplifica as questões de controle de simultaneidade.

O diagrama é descrito no conteúdo.

Figura 7:   O sistema de elevador, mostrando objetos ativos aninhados

Considere, por exemplo, o sistema de elevador parcial representado acima. Cada elevador tem portas, um guindaste e um painel de controle. Cada um desses componentes é bem modelado por um objeto ativo simultâneo, no qual o objeto porta controla a abertura e o fechamento das portas do elevador, o objeto guindaste controla o posicionamento do elevador por meio do guindaste mecânico, e o objeto painel de controle monitora os botões de seleção de andar e os botões de abertura/fechamento de porta. O encapsulamento dos threads simultâneos de controle como objetos ativos leva a um software muito mais simples do que poderia ser obtido se todo esse comportamento fosse gerenciado por um único thread de controle.

O Problema 'Estado Consistente' em Objetos

Como já dissemos quando abordamos as condições de competição, para que um sistema se comporte de maneira correta e previsível, determinadas operações dependentes de estado devem ser atômicas.

Para que um objeto se comporte adequadamente, é certamente necessário que seu estado seja internamente consistente antes e após o processamento de qualquer mensagem. Durante o processamento de uma mensagem, o estado do objeto pode estar em uma condição temporária e pode estar indeterminado porque, talvez, as operações estejam apenas parcialmente concluídas.

Se um objeto sempre conclui sua resposta a uma mensagem antes de responder a uma outra, a condição temporária não é um problema. A interrupção de um objeto para executar outro também não é problema, pois cada objeto executa um encapsulamento estrito de seu estado. (Para sermos mais específicos, isso não é totalmente verdade, como explicaremos em breve.)

Qualquer circunstância em que um objeto interrompa o processamento de uma mensagem para processar outra abrirá a possibilidade para as condições de competição e, portanto, requer o uso dos controles de simultaneidade. Isso, por sua vez, abrirá a possibilidade para o deadlock.

Portanto, o design simultâneo será geralmente mais simples se os objetos processarem cada mensagem de modo que ela seja concluída antes da aceitação de uma outra. Esse comportamento está implícito na forma particular do modelo de objeto ativo que apresentamos.

A questão do estado consistente pode se manifestar de duas formas diferentes nos sistemas simultâneos e, talvez, elas sejam mais fáceis de compreender quando consideramos os sistemas simultâneos orientados a objetos. A primeira forma é aquela que já abordamos. Se o estado de um único objeto (passivo ou ativo) for acessível a mais de um thread de controle, as operações atômicas deverão ser protegidas pela atomicidade natural das operações de CPU elementares ou por um mecanismo de controle de simultaneidade.

A segunda forma da questão do estado consistente é talvez mais sutil. Se mais de um objeto (ativo ou passivo) contiver as mesmas informações de estado, os objetos inevitavelmente terão estados diferentes por, pelo menos, curtos intervalos de tempo.  Em um design simples, eles podem ter estados diferentes por períodos mais longos, e até mesmo para sempre. Essa manifestação do estado inconsistente pode ser considerada um "duplo" matemático da outra forma.

Por exemplo, o sistema de controle de impulso do elevador (o guindaste) deve assegurar que as portas estão fechadas e não podem ser abertas antes que o elevador possa se movimentar. Um design sem garantias adequadas poderia permitir que as portas fossem abertas em resposta a um passageiro que tenha pressionado o botão de abertura de porta exatamente quando o elevador começou a se movimentar.

Pode parecer que uma solução fácil para o problema seja permitir que as informações de estado residam somente em um único objeto. Embora isso possa ajudar, essa medida pode causar também um impacto prejudicial no desempenho, particularmente em um sistema distribuído. Além do mais, essa não é uma solução infalível. Mesmo se somente um objeto contiver determinadas informações de estado, contanto que outros objetos simultâneos tomem decisões com base nesse estado em um determinado momento, as mudanças de estado poderão invalidar as decisões de outros objetos.

Não existe uma solução mágica para o problema do estado consistente. Todas as soluções práticas exigem que identifiquemos operações atômicas e as protejamos com algum tipo de mecanismo de sincronização que bloqueie o acesso simultâneo por períodos toleravelmente curtos. A expressão "toleravelmente curto" depende bastante do contexto. Esse tempo pode durar até que a CPU armazene todos os bytes em um número de ponto flutuante ou até que o elevador chegue à próxima parada.

Sistemas em Tempo Real

Nos sistemas em tempo real, o RUP recomenda o uso de Cápsulas para representar objetos ativos. As cápsulas possuem uma semântica forte para simplificar a modelagem da simultaneidade:
  • elas utilizam a comunicação baseada em mensagem assíncrona por meio de Portas utilizando Protocolos bem definidos;
  • elas usam uma semântica de execução-conclusão no processamento de mensagens;
  • elas encapsulam objetos passivos (assegurando, assim, que a interferência de threads não poderá ocorrer).