Código-Exemplo
5.3 Sincronização e Concorrência
5.3.1 Aplicação Produtor-Consumidor com buffer Limitado
Na descrição da arquitetura desta aplicação, procuramos distinguir as funções de sincronização e concorrência das demais funções da aplicação, tratando-as como propriedades não-funcionais. Neste contexto, os componentes Produtor,
131 produzir itens, consumir itens e armazenar itens.
Além dos três componentes básicos, a aplicação possui um conector, o qual denominamos Guarda. Este conector é responsável por controlar a sincronização e a concorrência junto ao componente Buffer. A figura 5-8 mostra a arquitetura.
estado put get estado_sai put_sai put_ent get_sai get_ent Produtor Consumidor Buffer Guarda
Figura 5-8 Arquitetura da aplicação Produtor-Consumidor com buffer limitado. A porta de saída
estado_sairecupera o estado do buffer.
Buffer possui três portas de entrada. A primeira (put) fornece o serviço de inserção de itens, a segunda (estado) permite a recuperação do estado do buffer (cheio, vazio, etc.) e a terceira (get) oferece o serviço de recuperação de itens.
O conector Guarda tem a função de controlar o acesso ao Buffer feito pelos componentes Produtor e Consumidor. Ele age sincronizando o acesso, ou seja, impedindo por exemplo que o Produtor tente produzir itens quando o Buffer estiver cheio, ou que o Consumidor tente consumir itens quando o Buffer estiver vazio. O conector também age controlando a concorrência no acesso ao Buffer, ou seja, impedindo que Consumidor e Produtor acessem determinado item ao mesmo tempo. O cuidado com a concorrência preserva a integridade dos dados armazenados pelo Buffer.
132
às funções por ele tratadas. Cada vez que o Produtor solicita ao Buffer a inserção de determinado item, o conector intercepta a solicitação através de sua porta de entrada
put_ent. O conector então verifica o estado do Buffer através de sua porta de saída
estado_sai, e permite a continuidade da solicitação, ou não, de acordo com o estado recuperado. Rotina similar aplica-se ao Consumidor quando de sua solicitação de recuperação de itens junto ao Buffer.
5.3.1.1 Implementação
Na implementação da configuração relacionada à arquitetura da figura 5-8 baseamo-nos no design pattern proposto nesta dissertação e empregamos mais uma vez a linguagem Java. Esta linguagem permite o tratamento de concorrência através da cláusula synchronized, a qual não autoriza o acesso simultâneo a um determinado código por ela definido.
O componente Buffer age como um servidor, implementando as operações put(), get() e estado(). Produtor e Consumidor, por outro lado, invocam as operações do Buffer put() e get() respectivamente, agindo como componentes clientes. Estes são instanciados como threads com o objetivo de concorrer no acesso ao Buffer.
O conector Guarda foi implementado com base no design pattern Object
Synchronizer [HFR99]. Possui 7 operações dentre elas a operação handle(), responsável por receber a informação referente à porta de entrada que será estimulada e a requisição oriunda dos produtores e consumidores. Guarda está no código 5-6.
133
4 Object result = null;
5 String operacao = Configuracao.toOper(portaEnt);
6 if (operacao == "get_ent") // invoca operacao
7 result = this.get_ent(req); // correspondente ‘a porta 8 if (operacao == "put_ent")
9 result = this.put_ent(req); 10 return result;
11 } 12
13 public Object get_ent(Requisicao req) {
14 this.pre_get(); // verifica estado do buffer antes do get 15
16 String portaSai = "get_sai"; // encaminhando req 17 Object result = this.forward(portaSai, req); // p/ porta de saida 18 // get_sai
19 this.pos_get(); // libera outras threads 20 return result;
21 } 22
23 public Object put_ent(Requisicao req) {
24 this.pre_put(); // verifica estado do buffer antes do put 25
26 String portaSai = "put_sai"; // encaminhando req 27 Object result = this.forward(portaSai, req); // p/ porta de saida 28 // put_sai
29 this.pos_put(); // libera outras threads 30 return result;
31 } 32
33 public synchronized void pre_get() { 34 try {
35 String portaSai = "estado_sai"; // checa estado do 36 String estado = (String) this.forward(portaSai); // buffer atraves da 37 // porta estado_sai 38 if (estado == "vazio") wait(); // bloqueia thread
39 } catch (InterruptedException e) { System.out.println (e)}; 40 }
41
42 public synchronized void pos_get() {
43 String portaSai = "estado_sai"; //checa estado do 44 String estado = (String) this.forward(portaSai); // buffer atraves da 45 // porta estado_sai 46 if (estado == "ok") notifyAll(); // desbloqueia threads
47 } 48
49 public synchronized void pre_put() { 50 try {
51 String portaSai = "estado_sai"; //checa estado do 52 String estado = (String) this.forward(portaSai); // buffer atraves da 53 // porta estado_sai 54 if (estado == "cheio") wait(); // bloqueia thread
55 } catch (InterruptedException e) { System.out.println (e); } 56 }
57
58 public synchronized void pos_put() {
59 String portaSai = "estado_sai"; //checa estado do 60 String estado = (String) this.forward(portaSai); // buffer atraves da 61 // porta estado_sai 62 if (estado == "ok") notifyAll(); // desbloqueia threads
63 }}
134
operação handle() (linha 3). Ela recebe, como nos demais exemplos, a porta de entrada a ser tratada e a requisição vinda do Produtor ou Consumidor. A partir do objeto
portaEnt, handle() obtém sua operação correspondente e a invoca (linhas 5 a 9).
Dessa forma, a funcionalidade de cada porta de entrada do conector fica claramente distinta e identificada.
Para cada porta de entrada há uma operação anterior (pre) (linhas 33 e 49) e posterior (pos) (linhas 42 e 58). Ambas operações pre - pre_get() e
pre_pos() - cuidam de verificar o estado do buffer e, conforme a situação, bloquear a thread correspondente, impedindo-a de prosseguir até que seja desbloqueada (linhas 38 e 54). As operações pos - pos_get() e pos_put()- também verificam o estado do buffer com o objetivo de desbloquear as threads já bloqueadas, se for o caso (linhas 46 e 62).
Importante ressaltar que em todas as operações pre e pos, ocorre o encaminhamento à porta de saída estado_sai, com o objetivo de recuperar-se o estado do buffer. O estado é retornado através da string estado e verificado normalmente. A utilização do design pattern resulta em flexibilidade ao projetista quando da necessidade de encaminhar-se informações através de portas de saída. Esse processo de encaminhamento é ainda transparente, tanto em relação aos clientes (produtores e consumidores) quanto em relação ao servidor (buffer).
135
A arquitetura da figura 5-8 poderia ser modificada para retratar a situação de distribuição dos componentes envolvidos. Neste contexto podemos definir, como exemplo, o componente Buffer com uma interface CORBA e manter sua instância em uma máquina distinta dos produtores e consumidores. A modificação quanto à arquitetura da aplicação Produtor-Consumidor estaria relacionada ao acréscimo do conector CorbaC, apresentado na seção 5.2.1. A figura 5-9 mostra a arquitetura modificada. CorbaC Produtor Consumidor Buffer Guarda
Figura 5-9 Arquitetura da aplicação Produtor-Consumidor modificada. CorbaC adapta-se de acordo com conectores e componentes que intermedia.
O conector CorbaC apresenta-se na figura 5-9 com seis portas, sendo três de entrada e três de saída. Entretanto sua implementação (códigos 5-1 e 5-2) não precisa ser alterada. As portas presentes no conector devem apenas ser definidas quando da configuração da arquitetura, sendo que estas informações ficam armazenadas nas classes Configuração e Porta do modelo UML do design pattern. Quaisquer que sejam as portas e o número delas configurado para o conector CorbaC, a funcionalidade é a mesma: fluxo de informações que chega por quaisquer das portas de entrada é encaminhada para uma das portas de saída via CORBA, como descrito na seção 5.2.1.
136
Buffer em conformidade com os padrões CORBA; (ii) alimentamos a Configuração com o conector CorbaC; (iii) interligamos as portas de saída do conector Guarda com as portas de entrada do conector CorbaC e (iv) interligamos as portas de saída do conector CorbaC com as portas de entrada do componente Buffer. Todo este processo foi realizado sem que quaisquer modificações fossem necessárias nas implementações dos conectores envolvidos.
O design pattern Architecture Configurator, portanto, facilita a evolução de sistemas a partir de componentes e conectores já definidos e implementados, bem como a reutilização dos mesmos em novas arquiteturas.