Diretrizes:
|
Abordagem |
Vantagens |
Desvantagens |
---|---|---|
Processo único, sem threads |
|
|
Processo único, threads múltiplos |
|
|
Processos múltiplos |
|
|
O caminho evolutivo normal é começar com uma arquitetura de processo único e ir adicionando processos para grupos de comportamentos que precisam ocorrer simultaneamente. Nesses agrupamentos mais amplos, considere as necessidades adicionais de simultaneidade, adicionando threads nos processos para aumentar a simultaneidade.
O ponto de partida é designar vários objetos ativos a um único encadeamento ou tarefa do sistema operacional, utilizando um planejador de objetos ativos com finalidade definida. Desse modo, geralmente é possível obter uma simulação bem reduzida de simultaneidade, embora, com um único encadeamento ou tarefa do sistema operacional, não seja possível tirar proveito de máquinas com várias CPUs. A decisão-chave é isolar o comportamento de bloqueio em encadeamentos separados, para que esse comportamento não se torne um gargalo. O resultado é a separação dos objetos ativos com comportamento de bloqueio em seus próprios threads do sistema operacional.
Infelizmente, como ocorre com muitas decisões sobre arquitetura, não há respostas fáceis. A solução envolve uma abordagem cuidadosamente elaborada. Pequenos protótipos de arquitetura podem ser usados para verificar as implicações de um determinado conjunto de opções. Ao desenvolver o protótipo arquitetural do processo, concentre-se em escalonar o número de processos até o máximo teoricamente aceito pelo sistema. Considere as seguintes questões:
Os objetos ativos podem comunicar-se entre si de forma síncrona ou assíncrona. A comunicação síncrona é útil porque simplifica colaborações complexas por meio de um seqüenciamento rigidamente controlado. Em outras palavras, enquanto um objeto ativo estiver executando uma etapa de execução-conclusão que envolva invocações síncronas de outros objetos ativos, as interações simultâneas iniciadas por outros objetos poderão ser ignoradas até que a seqüência completa seja concluída.
Apesar de útil em alguns casos, esse procedimento também pode ser problemático, porque pode fazer com que um evento mais importante, com prioridade mais alta (inversão de prioridade), tenha de esperar. Esse fator é exacerbado pela possibilidade de o objeto disparado de maneira síncrona poder se autobloquear enquanto espera a resposta para uma invocação síncrona que ele mesmo originou. Uma situação desse tipo pode gerar uma inversão de prioridade ilimitada. Nos casos mais extremos, se houver circularidade na cadeia de invocações síncronas, poderá ocorrer um deadlock.
As invocações assíncronas evitam esse problema permitindo tempos de resposta limitados. Entretanto, dependendo da arquitetura do software, a comunicação assíncrona costuma gerar códigos mais complexos, já que um objeto ativo pode ter de responder, em algum momento, a vários eventos assíncronos (cada um deles podendo iniciar uma seqüência complexa de interações assíncronas com outros objetos ativos). Essa implementação pode ser muito difícil e está sujeita a erros.
O uso da tecnologia de serviço de mensagens assíncrono com liberação de mensagens garantida pode simplificar a tarefa de programação do aplicativo. O aplicativo poderá continuar a operação, mesmo que a conexão de rede ou o aplicativo remoto não esteja disponível. O serviço de mensagens assíncrono não impossibilita sua utilização no modo síncrono. A tecnologia síncrona exigirá a disponibilização de uma conexão sempre que o aplicativo estiver disponível. Como a conexão existe, o processamento de confirmação pode se tornar uma tarefa mais fácil.
Embora a carga de alternância de contexto dos objetos ativos possa ser bem baixa, é possível que esse custo ainda seja inaceitável para alguns aplicativos. Geralmente, isso ocorre em situações nas quais grandes volumes de dados precisam ser processados em alta velocidade. Nesses casos, pode ser preciso recorrer ao uso de objetos passivos e técnicas de gerenciamento de simultaneidade mais tradicionais (e mais arriscadas), como os semáforos.
Essas considerações, no entanto, não implicam necessariamente em abandonar completamente a abordagem de objeto ativo. Mesmo com aplicativos que usem grandes volumes de dados, a parte sensível ao desempenho geralmente é relativamente pequena no sistema geral. Como conseqüência, o resto do sistema pode continuar tirando proveito do paradigma de objetos ativos.
Em termos gerais, o desempenho é apenas um dos critérios para o design de sistemas. Se o sistema for complexo, outros critérios (como manutenibilidade, facilidade para fazer mudanças, compreensibilidade e outros) serão tão ou mais importantes. A abordagem de objeto ativo é bem mais vantajosa, pois oculta grande parte da complexidade e do gerenciamento da simultaneidade, apesar de permitir que o design seja expresso em termos específicos do aplicativo, ao contrário de mecanismos específicos de tecnologias de nível inferior.
Componentes simultâneos que não interagem são um problema quase trivial. Praticamente todos os desafios de design têm a ver com interações entre atividades simultâneas. Por isso, devemos primeiro tentar entender as interações. Algumas perguntas a serem feitas:
Uma vez entendida a interação, vamos pensar nas maneiras de implementá-la. A implementação deve ser selecionada com a finalidade de obter o mais simples design compatível com as metas de desempenho do sistema. Os requisitos de desempenho, em geral, incluem na resposta a eventos gerados externamente tanto taxas gerais de transferência de dados como latência aceitável.
Não é uma prática recomendável incorporar suposições específicas sobre interfaces externas em um aplicativo inteiro. Também não é nada eficaz manter vários threads de controle bloqueados, à espera de um evento. O melhor é atribuir a um único objeto a tarefa dedicada de detectar o evento. Quando o evento ocorrer, o objeto poderá notificar os outros que precisarem saber do evento. Este design baseia-se em um padrão de design conhecido e aprovado, o padrão "Observador" [GAM94]. Para obter maior flexibilidade, ele pode ser facilmente estendido para o "Padrão Publicador-Assinante", em que um objeto publicador age como intermediário entre os detectores de eventos e os objetos interessados no evento ("assinantes") [BUS96].
As ações realizadas em um sistema podem ser disparadas por eventos gerados externamente. Um evento de grande importância gerado externamente pode ser a própria passagem do tempo, representada pelas batidas de um relógio. Outros eventos externos originam-se em dispositivos de entrada conectados a um hardware externo, como dispositivos de interface do usuário, sensores de processos e links de comunicação com outros sistemas.
Para que o software detecte um evento, ele deve estar bloqueado, à espera de uma interrupção, ou deve verificar periodicamente se ocorreu algum evento. Nesse último caso, o ciclo periódico talvez precise ser menor para evitar a perda de eventos rápidos ou de ocorrências múltiplas ou apenas para minimizar o período de latência entre a ocorrência e a detecção do evento.
O interessante é que, mesmo no caso de um evento mais raro, algum software precisa estar bloqueado, à sua espera, ou verificar com freqüência a existência desse evento. Entretanto, muitos (se não a maioria) dos eventos com os quais o sistema deve lidar são raros. Na maioria das vezes, em qualquer tipo de sistema, nada de significativo está realmente acontecendo.
O sistema de elevadores oferece bons exemplos sobre isso. Entre os eventos importantes na vida de um elevador estão a chamada para serviço, a seleção do piso, o bloqueio manual da porta por um usuário e a passagem de um piso para o outro. Alguns desses eventos exigem uma resposta muito pontual, mas todos eles são extremamente raros quando comparados à escala do tempo de resposta desejado.
Um único evento pode disparar várias ações, e as ações variam de acordo com o estado de diversos objetos. Além disso, diversas configurações de um sistema podem usar o mesmo evento de maneira diferente. Por exemplo, quando um elevador passa por um piso, o visor da cabine deve ser atualizado e o próprio elevador deve saber onde está, para que seja capaz de responder a novas chamadas e reconhecer as seleções de piso feitas pelos usuários. Pode haver ou não um visor de localização em cada piso.
A varredura é dispendiosa, pois requer que parte do sistema interrompa periodicamente o que está fazendo para verificar se ocorreu algum evento. Se o evento exigir resposta rápida, o sistema terá de verificar a ocorrência de eventos com bastante freqüência, limitando a realização de outros trabalhos.
É bem mais eficaz alocar uma interrupção para o evento, com o código referente ao evento ativado pela interrupção. Embora, às vezes, as interrupções sejam evitadas por serem consideradas "dispendiosas", elas podem ser muito mais eficientes quando utilizadas ponderadamente do que o polling repetido.
Os casos nos quais as interrupções são preferíveis como mecanismo de notificação de eventos são aqueles em que a chegada de eventos é aleatória e pouco freqüente, fazendo com que a maioria dos esforços de varredura não identifique a ocorrência do evento. Os casos nos quais a varredura é preferível são aqueles em que os eventos chegam de forma regular e previsível, e a maioria dos esforços de varredura identificam a ocorrência do evento. Há um ponto entre os dois métodos no qual é indiferente usar o comportamento de varredura ou o reativo. Os dois funcionarão bem e com pouca variação. Na maioria dos casos, porém, devido à aleatoriedade dos eventos no mundo real, o comportamento reativo é mais aconselhável.
A transmissão de dados (normalmente feita com a utilização de sinais) é dispendiosa e representa um desperdício, pois apenas poucos objetos podem estar interessados nos dados, mas todos (ou muitos) precisam ser interrompidos para verificá-los. Uma abordagem melhor e com menor consumo de recursos é a notificação para informar somente aos objetos interessados a ocorrência de algum evento. Restrinja a transmissão aos eventos que precisam da atenção de muitos objetos (geralmente eventos de tempo ou de sincronização).
Mais especificamente:
Talvez a diretriz mais importante para o desenvolvimento de aplicativos simultâneos eficientes seja maximizar o uso dos mecanismos de simultaneidade mais leves. Tanto o software do sistema operacional quanto o hardware desempenham um papel importante no suporte à simultaneidade. No entanto, ambos contêm mecanismos relativamente pesados, o que aumenta o trabalho do designer de aplicativos. Cabe a nós preencher a lacuna existente entre as ferramentas disponíveis e as necessidades dos aplicativos simultâneos.
Os objetos ativos ajudam a preencher essa lacuna por meio de dois recursos importantes:
Os objetos ativos também são um ambiente ideal para os objetos passivos fornecidos por linguagens de programação. Projetar um sistema completo a partir dos fundamentos de objetos simultâneos sem artefatos procedurais (como programas e processos) permite que os designs sejam mais modulares, coesos e compreensíveis.
Na maioria dos sistemas, menos de 10% do código usam mais de 90% dos ciclos da CPU.
Muitos designers de sistema agem como se todas as linhas de código tivessem de ser otimizadas. Prefira usar seu tempo para otimizar os 10% do código que são executados com mais freqüência ou que são mais demorados. Crie o design dos outros 90%, enfatizando aspectos como compreensibilidade, manutenibilidade, modularidade e facilidade de implementação.
Os requisitos não-funcionais e a arquitetura do sistema afetarão a escolha dos mecanismos utilizados para implementar chamadas de procedimentos remotos. Será apresentada abaixo uma visão geral dos tipos de intercâmbio existentes entre as alternativas.
Mecanismo | Usos | Comentários |
---|---|---|
Serviço de mensagens | Acesso assíncrono a servidores empresariais | O middleware do serviço de mensagens pode simplificar a tarefa de programação do aplicativo, pois trabalha com enfileiramentos, timeout e condições de recuperação e reinicialização. O middleware do serviço de mensagens também pode ser usado em um modo pseudo-síncrono. Normalmente, a tecnologia do serviço de mensagens pode suportar mensagens grandes. Algumas abordagens RPC podem apresentar limitações quanto ao tamanho das mensagens e exigir programação adicional para lidar com mensagens grandes. |
JDBC/ODBC | Chamadas de bancos de dados | São interfaces independentes do banco de dados, usadas para permitir que servlets Java ou programas de aplicativo enviem chamadas a bancos de dados que podem estar no mesmo servidor ou em um outro. |
Interfaces nativas | Chamadas de bancos de dados | Muitos fornecedores de bancos de dados implementaram interfaces nativas de programas de aplicativo em seus próprios bancos de dados, oferecendo uma vantagem de desempenho em relação ao ODBC à custa da portabilidade do aplicativo. |
Chamada de Procedimento Remoto | Chamar programas em servidores remotos | Talvez você precise programar no nível de RPC se tiver um desenvolvedor de aplicativos que cuide disso para você. |
Conversação | Pouco usado em aplicativos de comércio eletrônico | Em geral, é a comunicação entre programas de nível inferior que utilizam protocolos, como APPC ou Sockets. |
Diversos sistemas precisam de recursos de simultaneidade e de componentes distribuídos. A maioria das linguagens de programação oferece pouca ajuda a qualquer dessas questões. Vimos que precisamos de boas abstrações para compreender a necessidade da simultaneidade em aplicativos e as opções para implementá-la no software. Vimos também que, paradoxalmente, enquanto o software simultâneo é inerentemente mais complexo do que o não-simultâneo, ele também é capaz de simplificar bastante o design de sistemas que lidam com a simultaneidade no mundo real.
Rational Unified Process
|