MAC 499 - Trabalho de Formatura Supervisionado



Nome do aluno: Sérgio Haruo Nakanishi
Nome do supervisor: Flávio Soares Corrêa da Silva
Tema da monografia: Aplicação do MMASS em jogos
Tipo do trabalho: IC

Aviso
O documento HTML da monografia usa a codificação Unicode UTF-8 para símbolos matemáticos e pode precisar de (temporárias) mudanças de configurações do seu browser (em inglês).
Nos testes em minha máquina o Firefox 1.5 reconheceu o código automaticamente, sem a necessidade de configuração manual. No Internet Explorer 6.0 não consegui configurar o browser corretamente. Se Deus quiser, o Internet Explorer 7.0 conseguirá reconher o código automaticamente.

Índice

  • Bibliografia


  • Introdução [índice]

    Com o avanço tecnológico (que permite o desenvolvimento de aplicações complexas) e com o amadurecimento da IA, o interesse pelos sistemas multi-agentes (MAS) cresceu nos últimos anos. MASs são sistemas compostos de vários agentes autônomos que interagirem, de forma cooperativa ou competitiva, de modo a contribuir na realização de um comportamento geral.

    Atualmente existem várias ferramentas para o desenvolvimento de MASs. Citando alguns exemplos temos o Jade, o TuCSoN e o Swarm.

    O avanço tecnológico também inclui os jogos de vídeo-games. O progresso do hardware torna possível a realização de simulações do nosso ambiente cada vez mais próximo da realidade, tanto no aspecto visual quanto no comportamento das leis da física. Um mundo representado realisticamente exige personagens com comportamento convincentes. É neste ponto que a IA atua em um jogo. Um MAS aplicado em jogos pode ajudar a gerenciar o comportamento de um grupo de agentes que devem agir como uma equipe para alcançar um objetivo (por exemplo, jogadores de futebol em busca do gol) ou para realizar um comportamento geral (por exemplo, um grupo de soldados devem marchar organizadamente) que são cenas comum em um jogo atualmente.

    Lord of the rings
    Agentes trabalhando em conjunto, algo comum nos jogos atuais

    O italiano Giuseppe Vizzari desenvolveu um framework para a construção de sistemas multiagentes chamado Multilayered Multiagent Situated System (MMASS), que foi tema de sua tese de doutorado em 2003. O objetivo do meu trabalho foi estudar, implementar e aplicar o MMASS em jogos. Mas os estudos não se limitaram ao MMASS. Para implementá-lo foi preciso conhecer C++ e principalmente aprender as bibliotecas Standard Template Library, Pthread, Boost e Ogre3D. Com exceção de C++ e da Ogre3D, nesta monografia procurei documentar todos estes assuntos estudados, principalmente o MMASS.

    Na seção que se segue será apresentado este sistema e na seção seguinte será discutido pontos interessantes da sua implementação.

    MMASS [índice]

    Nesta seção vamos apresentar os conceitos e dar a definição do MMASS. Note que em alguns trechos as notações apresentadas não são rigorosamente seguidas em troca de clareza. As subseções Visão geral e Definições são um resumo traduzido do capitulo 2 de [1].

    Visão geral [índice]

    O MMASS é um framework formal e computacional para construção de sistemas multiagentes (MAS). Um MAS é formado por vários agentes autônomos que se interagem de forma a contribuir na realização de um comportamento geral.

    O MMASS é composto por várias camadas interconectadas (multilayered), cada uma delas podem representar diferentes aspectos do sistema que estamos modelando, como por exemplo, o espaço físico, mas também podem representar aspectos não relacionadas a noções físicas. Cada camada é um grafo conexo, os nós serão chamados de sítios, e as arestas formam o caminho por onde os agentes e campos podem se mover de um sítio para outro (veremos como mais adiante).

    Os agentes são posicionados sobre os sítios. Todo sítio abriga no máximo um agente (obedecendo a lei física da impenetrabilidade) e um agente ocupa apenas um nó em um dado instante (ele não é onipresente). Dois agentes são considerados adjacentes se eles ocupam sítios adjacentes (esta relação é a mesma de grafos onde dois nós são adjacentes se há uma aresta os ligando).

    A figura 1 ilustra os conceitos explicados até agora, temos uma camada e o agente A posicionado em um sítio.

    Figura 1
    Figura 1

    Os agentes podem realizar quatro tipos de ações, a saber, acionar, caminhar, reagir e emitir campo. Apenas as duas últimas são usadas para a interação entre agentes.

    A reação permite que um agente interaja com um conjunto de outros agentes adjacentes a ele. Os envolvidos irão mudar de estado interno desde que todos concordem na realização desta ação, preservando a autonomia de cada agente.

    A emissão de campo permite a interação de agentes distantes (ou não adjacentes). Uma boa analogia para esta ação são os fenômenos naturais de emissão de ondas (por exemplo, o som). Um campo é um sinal que se espalha pela camada assumindo algum valor em cada sítio de acordo com as regras relacionadas do campo. Este sinal pode alcançar algum agente que, de acordo com seu estado e sua capacidade de percepção (que estão relacionados ao tipo do agente), pode ou não perceber o sinal. Se for percebido, o sinal pode acionar alguma ação neste agente atingido, por exemplo, emitir outro sinal.

    Como já foi dito, este sistema é formado por várias camadas interconectadas por interfaces. A interface regula a passagem de campos de uma camada para outra. Isso significa que um agente consegue influenciar agentes de outras camadas através da emissão de campo. Como será visto nas definições, um agente não consegue mudar de camada.

    A figura 2 mostra três camadas, uma sobre a outra, e entre elas há duas interfaces ligando a camada de baixo com a de cima.

    Figura 2
    Figura 2

    O MMASS segue um modelo de interação indireto. Isso quer dizer que os agentes nunca mandam mensagens diretamente a outro agentes. Se eles quiserem interagir, a comunicação é feito através de um intermediário. No caso deste sistema, o intermediário é o próprio ambiente. Portanto, os agentes não são responsáveis pelo gerenciamento de suas ações, porém o ambiente deve fornecer toda a infraestrutura adequada para suportá-las.

    A figura 3 ilustra esta questão. Nela, o agente A notifica o ambiente da sua intenção de reagir com os agentes vizinhos, e então o ambiente é responsável em notificar os agentes vizinhos sobre a reação, além de executá-la caso seja aceita.

    Figura 2
    Figura 3

    Um conceito importante neste framework é o posicionamento (situated). Todo agente ocupa um lugar específico no ambiente, que influencia o comportamento dele, por exemplo, um agente posicionado em um sítio sem campo ativo (valor diferente de nulo) pode ter um comportamento diferente se ele estivesse ocupando um sítio com vários campos ativos.

    O modelo de interação indireto introduz naturalmente este conceito de posicionamento. No modelo de interação direto, os agentes comunicam-se diretamente através de mensagens. Para introduzir o conceito de espaço neste caso, é necessário um certo esforço para modificar o protocolo das mensagens e o conceito seria introduzido artificialmente no sistema.

    Esta subseção foi um resumo geral do MMASS. Apesar de ser um pouco incompleto, ela é fácil de entender e irá facilitar a leitura da próxima subseção sobre as definições formais.

    Definições [índice]

    Uma camada do MMASS é chamada de Multiagent Situated System (MASS).

    Definição 1: um MASS M é uma tripla

    <EspaçoM, FM, AM>

    onde EspaçoM representa o ambiente onde os agentes pertencentes ao grupo AM se posicionam, agem de forma autónoma e emitem campos pertencentes ao grupo FM.


    Definição 2: um espaço S é um par

    <PS, ES>

    onde:
    • PS ≠ ∅ é um conjunto finito de sítios.
    • ESPS × PS é uma relação simétrica, não reflexiva e não transitiva chamada de adjacência (que são representadas por arestas).
    • pPS, ∃ qPS | ∃ eES, e = (p, q) (um espaço é um grafo conexo).
    • dados dois espaços Si e Sj : SiSj, PiPj = ∅ (espaços diferentes não tem sítios em comum).

    O terceiro item, sobre o grafo ser conexo, parece estar errado, um contra-exemplo seria um grafo com os nós A,B,C e D, com uma aresta ligando A e B e uma aresta ligando C e D, este exemplo obedece o item em questão e mesmo assim não é conexo. Então o correto seria dizer que para qualquer par de nós, existe um caminho entre eles.


    Definição 3: cada sítio pP é uma tripla

    <ap, Fp, Pp>

    onde:
    • apA ∪ {⊥} é o agente situado em p (ap = ⊥ quando p está vazio).
    • FpF é o conjunto de campos ativos em p (Fp = ∅ quando não há campos ativos em p).
    • PpP | ∀ qPp, ∃ eE, e = (p, q) (Pp é o conjunto de sítios adjacentes a p).


    Definição 4: uma interface I(j, k) é uma tripla

    <pj, pk, Fτ>

    onde pjPj, pkPk com jk. Pj e Pk relativos aos espaços MASSj e MASSk respectivamente. Em relação ao tipo de campo Fτ os sítios indicados são considerados adjacentes, ou seja, o campo do tipo Fτ ao chegar em pj será difundido nos sítios adjacentes (Ppj) e também em pk.


    Definição 5: um caminho entre camadas IPath(i, j), entre as camadas MASSi e MASSj, é uma sequência de interfaces I(a1 , b1), ..., I(ak , bk) tal que

    • a1 = i e bn = j (o caminho começa de MASSi e termina em MASSj).
    • i : 1 < i < n, (bi - 1 = ai) ^ (bi = ai + 1) (toda interface intermediária i conecta o MASS de saída da interface anterior com o MASS de chegada da interface seguinte).

    Faça uma analogia com grafos. Se cada MASS for um nó, então as interfaces representam arestas. E assim, a definição 5 é a mesma definição de caminho entre dois nós de grafos.


    Definição 6: um Multilayered Multiagent Situated System (MMASS) é um par

    <M, I>

    onde:
    • M = {MASS1, ..., MASSn} é o conjunto de camadas que compõe o ambiente.
    • I = {I1, ..., In} é o conjunto de interfaces.
    • i, j ∈ ℕ, ij, 1 ≤ i, jn, ∃ IPath(i, j) = I(a1 , b1), ..., I(ak , bk) com I(a1 , b1), ..., I(ak , bk)I (não há camadas isoladas no sistema, este item é análogo à definição de grafo conexo, onde os MASSs representam os nós).
    O MMASS é um conjunto de camadas (MASSs) interconectadas por interfaces, que permitem a troca de sinais (campos) entre elas. Cada camada é um grafo onde sinais podem se difundir e serem percebidas pelos agentes posicionados nela.


    Definição 7: um tipo de campo Ft é uma quádrupla

    <Wt, Difundirt, Comparart, Comport>

    onde:
    • Wt é o conjunto de valores que o campo pode assumir. Normalmente este é um conjunto de ordem parcial, incluindo o campo nulo ⊙ cuja as propriedades são:
      • wtWt, Comport (wt, ⊙) = wt (elemento identidade para a função Comport).
      • wtWt, wt ≠ ⊙, Comparart(wt, ⊙) = True (elemento mínimo de Wt).
      Uma notação para o valor de campo será usado: fgp é o valor do campo de tipo Fg no sítio p. Valor do campo também pode ser chamado de intensidade do campo.
    • Difundirt : P × Wt × P → (Wt)+ é a função de difusão que nos diz os valores de um campo em um dado sítio, levando em consideração em qual sítio e com que valor o campo foi gerado (veja o exemplo abaixo).
    • Comport : (Wt)+Wt é a função de composição que diz como dois campos de mesmo tipo são combinados, esta função é comutativa.
    • Comparart : Wt × Wt → {True, False} é a função de comparação que compara valores de campos.

    Campos são sinais que os agentes podem emitir, eles espalham-se no ambiente assumindo algum valor em cada sítio calculado pela função Difundirt. Um exemplo dessa função seria:
    Difundirt(po, fgpo, p) = fgpo / (1 + dist(po, p)), se dist(po, p) < fgpo, caso contrário é 0
    onde po é o sítio onde o campo foi emitido, fgpo o valor com que o campo foi originado e p o sítio para o qual queremos calcular o valor.

    A figura abaixo ilustra o comportamento de um campo. O pico do gráfico é o ponto de emissão e onde o campo tem o valor mais alto, conforme vamos nos afastando deste ponto, o valor do campo vai diminuindo.

    Figura 1
    Figura 4

    Os campos podem persistir no ambiente logo após sua emissão ou desaparecer, tal comportamento é definido pela aplicação.

    A comunicação por campos é intrinsecamente multicast, os receptores dos sinais são desconhecidos.

    Dado F o conjunto de todos os campos em um MMASS, F = ∪t ∈ Τ Ft com Τ sendo o conjunto dos "rótulos" de todos os tipos de campo, e FtiFtj quando ij.


    Definição 8: um agente aA é uma tripla

    <s, p, τ>

    onde:
    • s ∈ Στ é o estado do agente.
    • pP é o sítio do MASS onde o agente está situado.
    • τ é o tipo do agente, que descreve o conjunto de estados que o agente pode assumir, uma função para determinar a sensibilidade dos agentes aos campos, e as ações que o agente pode realizar.
    Cada MASS é povoado por um conjunto de agentes A. Um agente a possui um estado e uma posição (denotado por pa), que são seus aspectos dinâmicos, e também possui um tipo (denotado por τa), que é seu aspecto estático.


    Definição 9: um tipo de agente τ é uma tripla

    τ, Percepçãoτ, Açãoτ>

    onde:
    • Στ é o conjunto de estados que agentes do tipo τ podem assumir.
    • Percepçãoτ : Στ → [N × Wf1] ... [N × Wf|Τ|] é uma função que relaciona cada estado ao vetor de pares
      (c1τ(s), t1τ(s)), (c2τ(s), t2τ(s)), ..., (c|Τ|τ(s), t|Τ|τ(s))

      onde para cada i (i = 1, ..., |Τ|), ciτ(s) e tiτ(s) são, respectivamente, o coeficiente de sensibilidade e o limite de sensibilidade para campos do tipo i. Um agente do tipo τ no estado s ∈ Στ pode perceber um campo Fi se e somente se
      Comparari (ciτ(s) • wfi, tiτ(s)) = True

      ou seja, quando o primeiro componente do i-ésimo par de Percepçãoτ (o coeficiente ciτ(s)) multiplicado pela intensidade do campo percebido (wi) é maior que o segundo componente do par (o limite tiτ(s)).
    • Açãoτ é o conjunto de ações que constitui o comportamento para os agentes do tipo τ

    Note que o coeficiente ciτ(s) é um fator de aumento da intensidade do campo, ou seja, se um agente estiver no estado s e posicionado no sítio p, e este sítio tem um campo ativo de valor fgp, então o valor percebido pelo agente será cgτ(s) • fgp.

    O tiτ(s) é o valor de campo mínimo que um agente conseque sentir para o tipo de campo correspondente a Fi, ou seja, se o agente sentir um campo abaixo deste valor, é como se ele não existisse.

    Vemos aqui que na comunicação por campos os receptores (os agentes que perceberão os campos) são determinados pelos seus tipos, estados e posições.

    O conjunto Açãoτ determina como que um agente do tipo τ pode mudar seu estado e posição e como interagir com agentes vizinhos e distantes. A seguir é explicado como que as ações conseguem este efeito.


    As próximas quatro definições são as ações que os agentes podem realizar, são eles acionar, caminhar, reagir e emitir. Acionar permite que os agentes mudem de estado, enquanto caminhar permite a mudança de posição. Reação define uma interação entre agentes vizinhos de acordo com um processo de duas etapas, primeiro os agentes devem concordar em participar da reação, depois os agentes devem estar em condições de escolher uma ação para a reação. Um agente também pode emitir um campo quando alguma condição o permitir.

    As definições das ações seguem a forma ação-cond-efeito. Ação é uma expressão do tipo f(x1, ..., xn), onde f é o nome da ação e xi são termos que podem aparecer em cond e/ou em efeito. Cond é a condição necessária para que esta ação possa ocorrer (mas isso não implica que esta ação seja escolhida de fato pelo agente as condições sejam satisfeitas). Efeito é o estado que o agente ou seu sítio estará depois que a ação for concretizada.

    Devido à autonomia, não é possível dizer a um agente algo sobre as condições de outros agentes (por exemplo, informações sobre o estado interno deles), exceto por fatos observáveis como, por exemplo, quais agentes são adjacentes ou se concordaram em participar de uma reação. Além disso, os efeitos das ações podem envolver somente uma mudança no estado do agente que está realizando a ação ou podem influenciar o ambiente por uma modificação local como, por exemplo, a emissão de um campo ou a mudança do estado de um sítio pela ação caminhar (estes efeitos serão vistos a seguir).

    Definição 10: uma ação acionar é definida como:

    • ação: acionar(s, s')
    • cond: a = <s, p, τ> [, Scond(s), percebe(ft), Fcond(ft), ∃ a1, ..., aiA | (pa1, ..., paiPp, τa1 = τ1, ..., τai = τi, Srel(p, pa1, ..., pai))]
    • efeito: a = <s', p, τ>
    onde a = <s, p, τ> é o estado atual do agente. Nem todos os parâmetros são necessários, por exemplo, é possível ter uma ação acionar que é ativado para qualquer estado e posição. Algumas condições opcionais podem ser feitas, por exemplo, impor que o estado s do agente satisfaça determinadas condições, ou seja, Scond(s) deve ser satisfeito para ativar a ação (restrição sobre o estado). Outra opção é exigir que certo campo seja detectado (percebe(ft) que é satisfeito quando ft é a intensidade do campo tipo Ft, FtFp e Comparart (ctτ(s) • wft, ttτ(s)) = True). A outra opção é em relação aos agentes adjacentes a a, que devem ser de certos tipos e obedecerem a relação Srel(p, pa1, ..., pai). O efeito desta ação é mudar o estado do agente de s para s'.


    Definição 11: uma ação caminhar é definida como:

    • ação: caminhar(p, q [, ft, s, a1, ..., ai])
    • cond: a = <s, p, τ>, aq = ⊥, qPp [, percebe(ft), Fcond(ft), ∃ a1, ..., aiA | (pa1, ..., paiPp ^ τa1 = τ1, ..., τai = τi ^ Srel(p, pa1, ..., pai)), Frel(p, Pp)]
    • efeito: posição(q), vazio(p)
    onde as condições básicas são satisfeitas quando p é a posição do agente, qPp e q = <⊥, Fq, Pq> (q está vazio e é adjacente a p). As condições opcionais percebe(ft), Fcond(ft) e Srel(p, pa1, ..., pai) já foram explicadas em acionar. A condição Frel(p, Pp) pode ser usado para especificar uma relação sobre os valores de campos ativos nos sítios vizinhos que deve ser satisfeita (por exemplo, o sítio de destino deve ser aquele com o maior valor de um campo dado). O efeito desta ação é mudar a posição do agente que a executou, e consequentemente mudar o espaço (p = <⊥, Fp, Pp> e q = <a, Fq, Pq>).


    Definição 12: uma ação emitir campo é definido como:

    • ação: emitir(p [, s, ft, a1, ..., ai])
    • cond: a = <s, p, τ>, [, percebe(ft), ∃ a1, ..., aiA | (pa1, ..., paiPp ^ τa1 = τ1, ..., τai = τi ^ Srel(p, pa1, ..., pai))]
    • efeito: adicionado(f, p)
    onde as condições são análogas com aquelas explicadas nas ações anteriores. O efeito é a modificação, determinada pela função Difundirt, no estado dos sítios da camada. Seja Pe = {qP | Difundirt(p, ft, q) ≠ ⊙} o conjunto dos sítios que serão afetados por esta ação, dado qPe, o conjunto F'q de campos ativos em q após a difusão do campo será:
    • Fq ∪ {<Difundirt(p, ft, q), Comport, Comparart>, se Fq não tinha o tipo de campo Ft.
    • (Fq - {f't}) ∪ {<Comport (Difundirt(p, f't, q), w't), Comport, Comparart>} onde f't = <w't, Comport, Comparart> era o único campo não nulo do tipo Ft em q antes da emissão.


    Definição 13: uma ação reação é definida como:

    • ação: reação(s, ap1, ..., api, s')
    • cond: a = <s, p, τ>, p1, ..., piPp, concordou(ap1, ..., api) [, Scond(s), τap1 = τ1, ..., τapi = τi]
    • efeito: a = <s', p, τ>
    onde as condições, exceto concordou, são análogas às ações anteriores. Concordou(ap1, ..., api) especifica que um processo de sincronização foi realizada com sucesso e os agentes envolvidos (ap1, ..., api) concordaram em mudar de estado. O efeito desta ação é a mudança sincronizada de estado de todos os agentes envolvidos, em particular o agente a que realizou a ação.

    Estas foram todas as definições formais do MMASS. Essas definições não descrevem o sistema por completo. Resta mostrar o mecanismo de gerenciamento das ações. É mais conveniente descrever esse mecanismo junto com seu código e por isso será explicado na próxima subseção sobre a implementação.

    Implementação [índice]

    O trabalho do Vizzari discute sobre várias possibilidades de implementação, entre elas escolhi por fazer uma implementação multithread sincronizada. Neste caso, cada agente possui uma thread, assim como o ambiente, e cada thread possui um looping (while) onde cada iteração corresponde a um turno, então as threads ficarão sincronizadas por este turno. Mas antes de descrever a implementação, será feito uma introdução sobre as bibliotecas usadas.

    Bibliotecas usadas

    Nesta seção irei mostrar alguns códigos em C++ que utilizam a Standard Template Library (STL), algumas funções de Pthread e as classes shared_ptr e weak_ptr da biblioteca Boost. Como este texto está sendo escrito para alunos de BCC do IME, suponho que os leitores não possuam experiência com estas bibliotecas, e portanto irei escrever breves introduções sobre cada uma delas. As introduções são voltadas para a prática com o objetivo de facilitar o entendimento dos códigos aqui apresentados. A parte conceitual das bibliotecas já são conhecidas pelos alunos (principalmente no caso da Pthreads, onde uso mutex e variável de condição).

    Pthread

    Pthread é uma biblioteca em C que segue o padrão IEEE POSIX 1003.1c. Ele define um conjunto de tipos (structure) e funções para a criação e sincronização de threads. Sempre farei analogia com Java quando possível para facilitar a explicação.

    Assim como em Java, onde o comportamento de uma thread é definido no método run() da interface Runnable, em Pthread o comportamento da thread também é definido em uma função, porém, ao contrário de Java, esta função pode receber e retornar um valor do tipo void*, ou seja, qualquer tipo de valor.

    Uma thread é representada pela estrutura pthread_t. Podemos configurar alguns atributos dela através de uma outra estrutura do tipo pthread_attr_t. Neste trabalho o único atributo que usei foi o joinable que diz se o processo que criou a thread pode esperar pelo termino desta (como o método join() da classe Thread de Java).

    Para criar uma thread, usamos a função pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg), onde thread é a estrutura da thread e attr é a estrutura para configurá-la, se attr == NULL então é usado valores padrões, o terceiro parâmetro é o endereço de uma função que define o comportamento da thread e seu argumento é o parâmetro arg.

    Para esperar o termino de uma thread é usado a função thread_join(pthread_t thread, void **value_ptr), onde o parâmetro thread é a estrutura da thread e o retorno dela é guardada em value_ptr.

    Segue abaixo um exemplo simples para ilustrar seu uso:

    
    /* O valor de retorno da função da thread deve sempre ser do tipo void*
    assim como seu parâmetro. A função pthread_exit é o retorno da função,
    é equivalente ao return. */
    void* print_num(void *arg) {
       int num = *(int*)arg;
    
       printf("Num = %i\n", num);	
    
       pthread_exit(NULL);
    }
    
    
    int main() {
       pthread_t thread;
       int i = 2;
    	
       pthread_create(&thread, NULL, print_num, (void*)&i);
       // O valor de retorno está sendo ignorado.
       pthread_join(thread, NULL);
       
       return 0;
    }
    

    Para passar dois ou mais argumentos para a função da thread, precisamos criar uma estrutura (ou uma classe) contendo todos os parâmetros e passá-lo como argumento na pthread_create.

    Para a sincronização entre as threads podemos usar a estrutura pthread_mutex_t que representa um mutex, antes de usá-lo devemos inicializar seus atributos com a função pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr), onde o primeiro parâmetro é o mutex e o segundo parâmetro é uma estrutura usada para inicializar os atributos, se attr == NULL então valores padrões serão usados.

    Uma pthread_mutex_t é manipulada através da função pthread_mutex_lock(pthread_mutex_t *mutex). (Aqui é importante relembrar que um mutex é um semáforo binário) Quando esta função é executada por uma thread, se o valor do mutex é 1 então ela espera até que o valor do mutex mude para 0, se o valor do mutex é 0 então seu valor é mudado para 1 e a thread prossegue executando o comando seguinte, ou seja, a thread tenta "adquirir" o mutex. A função pthread_mutex_unlock(pthread_mutex_t *mutex) é usado para atribuir 0 ao mutex, ou seja, "liberar" o uso do mutex para outras threads. Portanto, seções críticas devem estar entre estas duas funções para garantir a exclusão mutua.

    O exemplo abaixo ilustra o uso de mutexes:

    		
    pthread_mutex_t mutex; // Visível por todas as threads.
    
    /* Tanto a thread A quanto a thread B faz uma leitura ou escrita na mesma
    variável e portanto essas operações formam a seção critica dessas
    threads. (Ignorando o fato de que a leitura e escrita de um int é
    geralmente feito atomicamente) */
    void* thread_A(void *arg) {
       int j, *i = (int*)arg;
       
       pthread_mutex_lock(&mutex);
       j = i * 2;
       pthread_mutex_unlock(&mutex);
    
       pthread_exit(NULL);
    }
    
    void* thread_B(void *arg) {
       int k, i = (int*)arg;
       k = random_number();
    
       pthread_mutex_lock(&mutex);
       i = k * 3;
       pthread_mutex_unlock(&mutex);
    
       pthread_exit(NULL);
    }
    
    int main() {
       pthread_t threadA, threadB;
       int i = 0;
       
       pthread_mutex_init(&mutex, NULL);
       pthread_create(&threadA, NULL, thread_A, (void*)&i);
       pthread_create(&threadB, NULL, thread_B, (void*)&i);
       
       pthread_join(threadA, NULL);
       pthread_join(threadB, NULL);
       
       return 0;
    }
    

    Fazendo uma analogia com Java, o uso de mutexes corresponde ao uso de blocos ou funções sincronizadas (synchronized). Então, os códigos abaixo são equivalentes.

    		
       // Código em C.
       pthread_mutex_lock(&mutex); // Adquire o mutex
       j = i * 2;
       pthread_mutex_unlock(&mutex); // Libera o mutex
    
       // Código em java.
       synchronized (object) { // Adquire o lock de object
          j = i * 2;
       } // Libera o lock de object
    

    A estrutura pthread_cond_t corresponde a uma variável de condição. Sua inicialização é feita através da função pthread_cond_init(pthread_cond_t *condition, pthread_condattr_t *attr) que é análogo à inicialização do mutex.

    Uma pthread_cond_t é usada em conjunto com uma pthread_mutex_t, ou seja, há sempre um mutex associado a uma variável de condição. Isso quer dizer que, antes de fazer qualquer operação sobre a variável de condição, a thread deve primeiro adquirir o mutex associado.

    Três funções são usadas para manipular uma pthread_cond_t. A função pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) faz com que a thread que a executa entre em estado de espera na variável cond e faz a associação do mutex com esta variável. Sempre que uma thread entra em estado de espera em uma variável de condição, ela libera automaticamente o mutex correspondente (outros mutexes devem ser liberados manualmente antes de entrar em espera para evitar deadlock).

    A função pthread_cond_signal(pthread_cond_t *cond) tira alguma thread do estado de espera na variável cond. Para esta função ter o efeito esperado, primeiro deve ser adquirido o mutex corresponde à variável cond e a sinalização só tem efeito depois que o mutex é liberado. Analogamente, a função pthread_cond_broadcast(pthread_cond_t *cond) acorda todas as threads que estão em estado de espera na variável cond.

    O exemplo abaixo ilustra o uso dessas três funções:

    		
    pthread_mutex_t mutex; // Visível por todas as threads.
    pthread_cond_t cond;  // Visível por todas as threads.
    
    /* Todas as threads deste função irão esperar pelo valor de i ser
     diferente de 1. */
    void* some_thread(void *arg) {
       int *i = (int*)arg;
       
       pthread_mutex_lock(&mutex);
       i = 1;
       while (i == 1) {
          // mutex será liberado automaticamente.
          pthread_cond_wait(&cond, &mutex);
          // mutex foi adiquirido automaticamente.
       }
       pthread_mutex_unlock(&mutex);
       
       pthread_exit(NULL);
    }
    
    int main() {
       pthread_t threads[3];
       int j, i = 0;
       
       pthread_mutex_init(&mutex, NULL);
       pthread_cond_init(&cond, NULL);
       
       for (j = 0; j < 3; j++)
          pthread_create(&threads[j], NULL, some_thread, (void*)&i);
          
       wait(1000); // espera 1 segundo
       
       pthread_mutex_lock(&mutex);
       i = 0;
       pthread_cond_broadcast(&cond);
       // ou pthread_cond_signal(&cond) para acordar somente uma thread
       pthread_mutex_unlock(&mutex); /* Somente aqui as thread são
       sinalizadas de fato. */
       
       return 0;
    }
    

    Comparando com Java, o uso de variável de condição corresponde ao uso dos métodos wait(), signal() e signallAll(). Portanto, os códigos abaixo são equivalentes.

       // Esperar em C
       pthread_mutex_lock(&mutex); // Adquire o mutex
       pthread_cond_wait(&cond, &mutex); // Espera e libera o mutex
       // Readquire o mutex
       pthread_mutex_lock(&mutex); // libera o mutex
       
       // Esperar em Java
       synchonized (obj) { // Adquire o lock
          obj.wait(); // Espera e libera o lock
          // Readquire o lock
       } // libera o lock
       
       // Sinalizar em C
       pthread_mutex_lock(&mutex); // Adquire o mutex
       pthread_cond_singal(&cond); // Sinaliza alguma thread
       pthread_mutex_lock(&mutex); // libera o mutex
       
       // Sinalizar em Java
       synchonized (obj) { // Adquire o lock
          obj.signal(); // Sinaliza alguma thread
       } // libera o lock
    

    Templates e Standard Template Library (STL)

    Em Java, toda classe é derivada de Object, dessa forma a criação de containers genéricos é facilitada, basta que eles abriguem objetos do tipo Object. Mas em C++ as classes não possuem necessariamente um ancestral em comum e portanto não é possível adotar esta mesma solução. A criação de Templates surgiu como uma alternativa para resolver este problema [2].

    Template é um modo de reutilização de código, assim como a herança, delegação e composição de classes também são. Entretanto, ele não faz parte do paradigma de programação orientada a objeto, na verdade é um mecanismo da própria linguagem C++ baseado no pré-processamento de macros da linguagem C. Ou seja, é um mecanismo realizado pelo compilador em tempo de compilação, e não pelo próprio programa em tempo de execução. O código abaixo mostra um exemplo de como criar um classe template.

    
       template<typename T>
       class Example {
          T value;
          
       public:
          T getValue() { return value; }
          void setValue(T val) { value = val; }
       };
    

    O símbolo T é chamado de parâmetro do template. Note que T é indefinido (ele não é uma classe nem um tipo primitivo) e portanto não podemos utilizar esta classe (instanciar, derivar, etc) sem definir todos os seus parâmetros. O código abaixo mostra um exemplo de uso do template acima.

    
    int i = 1;
    Example<int> e1; // (1)
    e1.setValue(10);
    i = i + e1.getValue();
    
    string s = "but";
    Example<string> e2; // (2)
    e2.setValue("ters");
    s = s + e2.getValue();
    

    Quando encontra a linha (1), o compilador substitui o símbolo T por int e compila a classe. Portanto, para cada diferente parâmetro temos uma classe diferente. No código acima, Example<int> e Example<string> são duas classes distintas no sentido delas terem seus próprios códigos.

    Para este trabalho isso já é suficiente sobre template. Porém, ele é um mecanismo bastante flexível, é possível até realizar meta-programação, mas com certas limitações pois este não é sua finalidade. Para aprofundar neste assunto leia [2].

    A STL é uma biblioteca de containers, algoritmos e iteradores. Quase todos seus componentes são templates. Neste trabalho, usei os containers set e map e a classe iterator (também usei a classe string, mas não irei explicá-la por ser parecido com Java).

    O set é um conjunto sem repetições e sem ordem, ou seja, inserir um objeto que já existe no set não tem nenhum efeito e os objetos não são necessariamente ordenados na ordem que são inseridos. Todos os containers são templates, no caso do set, o modo mais simples de utilizá-lo é fornecendo apenas um parâmetro que é o tipo de objeto que ele irá guardar. Segue abaixo um exemplo de utilização deste container.

    
    set<int> numbers; // Um conjunto vazio de numeros
    numbers.insert(0); // Insere o numero 0 no conjunto
    numbers.insert(1);
    numbers.insert(2);
    numbers.insert(1); // Sem efeito
    
    set<int> nums;
    nums = numbers; // nums tem uma copia de cada elemento de numbers
    nums.clear(); // Nao afeta o conjunto "numbers"
    

    O map é um conjunto de associações chave/valor sem repetições de chaves. Podemos enxergar um vetor de C como um map onde a chave é sempre um número (o número do índice). Por isso a sintaxe deste container imita o comportamento do vetor de C. A forma mais simples de declarar um map é definindo dois parâmetros para a template, o tipo da chave e o tipo do valor. O exemplo abaixo ilustra o seu uso.

    
    map<const string, int> months; // Um map vazio
    months["january"] = 1; // Insere a associacao january->1
    months["february"] = 4;
    months["march"] = 3;
    months["february"] = 2; // Sobreescreve o ultimo valor
    
    int month;
    month = months["january"]; // Pega o valor correspondente a chave january
    month = months["may"]; // CUIDADO! Sera criado uma associacao entre may
    // e algum numero (provavelmente 0) e este valor sera retornado. Este 
    // nem sempre e o comportamento desejado. Para evitar isso eu faço:
    if (months.count("june") > 0)
       month = months["june"];
    
    // Existem outros metodos para acessar e inserir elementos em um map, eu
    // normalmente uso estes.
    

    Na STL, todo container possui uma classe interna chamada iterator. Como seu nome diz, esta classe serve para iterar sobre os elementos do container. Em C++, um iterator é uma generalização de apontador, ou seja, são objetos que apontam para outro objeto. Por isso, todas as operações usada em um apontador é aceita por esta classe. Neste trabalho, basicamente usei o operador ++ para avançar para o próximo elemento e o operador * para consultar o valor apontado pelo iterador (na verdade, a STL define vários conceitos de iteradores que não serão abordados aqui). Iteradores para o mesmo container podem ser comparados através dos operadores == e !=, dois iterators são iguais se eles apontam para o mesmo elemento, caso contrário são diferentes. Finalmente, para pegar um iterador que aponta para o primeiro elemento do container é usado o método begin(), para pegar um iterador que aponta para o elemento seguinte ao último elemento do container é usado o método end(), note que o valor deste último iterador não deve ser consultado, seu uso é apenas para fins de comparação. O exemplo abaixo ilustra o uso do iterator da classe set.

    
    int total = 0;
    set<int> numbers;
    numbers.insert(0);
    numbers.insert(1);
    numbers.insert(2);
    
    set<int>::iterator i, end = numbers.end();
    for (i = numbers.begin(); i != end; ++i)
       total += total + (*i);
    

    No map, a história é um pouco diferente. Os elementos do map são do tipo pair que é o par chave/valor, então, se temos um map<const string, int> seus elementos são do tipo pair<const string, int>. A classe pair tem dois campos públicos: first e second, onde first é a chave e second o valor. O exemplo abaixo mostra o uso de iteradores com map.

    
    int odd = 0;
    bool hasJanuary = false;
    map<const string, int> months;
    months["january"] = 1;
    months["february"] = 2;
    months["march"] = 3;
    
    map<const string, int>::iterator i, end = months.end();
    for (i = months.begin(); i != end; ++i) {
       if (((*i).second % 2) != 0) odd++;
       if ((*i).first == "january") hasJanuary = true;
    }
    

    Boost: shared pointer & weak pointer

    A STL possui uma classe que faz o papel de um ponteiro inteligente chamada auto_ptr. Sua principal funcionalidade é tomar posse da referência para algum objeto alocado dinamicamente no heap, desse modo, quando o auto_ptr é destruído (por exemplo, por ter saído fora de escopo), a referência (nesta subseção, referência significa "o objeto apontado pelo apontador") também é destruída e sua memória liberada automaticamente (por isso se diz que o auto_ptr toma posse da referência). Esta classe foi criada para facilitar a vida de projetistas e programadores, mas algumas limitações não permitiam que ela fosse usada em muitos casos. Por isso, a biblioteca Boost incluiu extensões da classe auto_ptr tentando resolver estas limitações.

    A Boost foi iniciada por membros da C++ Standards Committee Library Working Group. Muitas de suas bibliotecas fazem parte do relatório técnico de bibliotecas deste comitê e são candidatas a fazer parte da STL. Neste trabalho usei as classes shared_ptr e weak_ptr.

    A shared_ptr toma posse da referência para algum objeto alocado dinamicamente, o que torna essa classe diferente é que ela pode dividir a posse com outros shared_ptr's. Para isso ela mantem um contador que guarda quantos shared_ptrs têm posse para a mesma referência, quando este contador chega a zero então o objeto é destruído e sua memória liberada automaticamente.

    Há casos em que a posse não deve ser compartilhada, por exemplo, vamos supor que um programador esteja manipulando objetos somente com apontadores inteligentes, se um objeto possuir uma auto-referência usando um shared_ptr, ele corre o risco de nunca ser destruído pois a auto-referência faz com que o contador sempre seja no mínimo 1. Neste caso devemos usar um weak_ptr que guarda uma referência para o objeto mas não toma posse dela, ou seja, ele não será contado como dono referência pelo shared_ptr.

    A classe shared_ptr foi projetada para ter referência de um único objeto, e não de um conjunto de objetos como os iteradores de containers. Portanto, aritmética de ponteiros não funciona nesta classe, apenas os operadores para "desreferenciar" funcionam nesta classe, ou seja, os operadores * e ->).

    No caso da weak_ptr, "desreferenciá-la" não seria uma operação segura pois o objeto referenciado pode ter sido destruído. Então, esta classe não serve para manipular diretamente os objetos, para isso primeiro é necessário conseguir uma shared_ptr contendo a mesma referência do weak_ptr. Esta classe possui um método justamente com esta finalizadade, o método lock.

    Segue abaixo um exemplo de uso das classes descritas.

    
    class Example {
       int value;
    public:
       int getValue() { return value; }
       void setValue(int val) { value = val; }
    };
    
    int main() {
       Example *ex = new Example; // Objeto alocado dinamicamente
       boost::shared_ptr<Example> ex1(ex);
       // O metodo use_count() da classe shared_ptr retorna o valor do
       // contador de quantas shared_ptrs dividem a posse de ex.
       if (ex1.use_count() != 1) return -1;
       
       boost::shared_ptr<Example> ex2(ex1);
       if (ex2.use_count() != 2) return -1;
       
       boost::shared_ptr<Example> ex3(ex);
       // CUIDADO! Note que no caso acima o ex3 nao divide a posse com os
       // outros shared_ptr's (ex1 e ex2). Portanto, se ela for destruida o
       // ex tambem sera, e os outros shared_ptr's terao uma referencia
       // perdida
       if (ex3.use_count != 1) return -1;
       
       ex3->setValue(10); // Repare na sintaxe parecida com a de apontadores
       // Apesar de nao dividir a posse com os apontadores anteriores
       // qualquer efeito colateral sobre o ex eh sentido por todos eles
       // por se tratar do mesmo objeto.
       if (((*ex1).getValue() != 10) ||
           ((*ex2).getValue() != 10) || 
           ((*ex3).getValue() != 10))
             return -1;
       
       // boost::weak_ptr ex4(ex);
       // Nao eh possivel instanciar um weak_ptr desta maneira pois
       // ele nao toma posse da referencia.
       // Todo weak_ptr eh derivado de um shared_ptr.
       boost::weak_ptr<Example> ex5(ex1);
       boost::weak_ptr<Example> ex6 = ex1;
       if (ex5.use_count() != 2) return -1;
       
       // (*ex5).setValue(5);
       // ex5->setValue(5);
       // Nao eh possivel "desreferenciar" um weak_ptr.
       boost::shared_ptr<Example> ex7 = ex5.lock();
       if (ex7.use_count() != 3) return -1;
       
       ex5.lock()->setValue(5);
       
       return 0;
    }
    

    Neste trabalho criei uma série de typedef's para facilitar a declaração de apontadores inteligentes. Todo apontador que tiver esta cara:
    boost::shared_ptr<NOME_DA_CLASSE>
    terá um typedef correspondente deste jeito:
    spNOME_DA_CLASSE
    a lógica do nome é: se começar com sp é um shared_ptr, se começar com wp é um weak_ptr. Um exemplo concreto é o spAgent que é um boost::shared_ptr<Agent>. Um set de spAgent é um spAgent_set, e um map<const string, spAgent> (onde a string é o nome do agente) é um spAgent_map, o mesmo vale para outras classes (Site, Field, Action, etc).

    Classes do MMASS

    Note que as definições revelam uma parte da arquitetura do sistema, ou seja, mostram classes e suas relações. Por exemplo, podemos ver que um objeto do tipo MMASS possui um conjunto de objetos do tipo interface e do tipo MASS, e que um objeto do tipo MASS é composto de um objeto do tipo espaço e de um conjunto de objetos do tipo agente. A figura 5 mostra o diagrama de classes simplificado deste trabalho. Note que nem todas as definições tem uma classe correspondente. A classe Interface não foi implementada pois a aplicação do trabalho não aproveita a característica de multiplas camadas do MMASS.

    Figura 2
    Figura 5

    Procurei implementar classes cujos os objetos não podem ser utilizados se não estiverem de acordo com suas definições. Por exemplo, um Space deve ter algum Site, e os Sites devem formar um grafo conexo. Deste exemplo, foi extraído um pedaço de código a ser comentado.

    Todos os códigos apresentados neste trabalho foram modificados ou para simplificá-los e melhorar sua legibilidade ou para não ter que explicar outras partes do sistema.

    
    void Space::checkGraph() throw (logic_error) {
       unsigned int numOfConnectedComponents = 0;
    
       if (sites.empty())
          throw logic_error("Um espaço deve ter algum sítio");
    
       spSite_map::iterator site_i, end = sites.end();
       for (site_i = sites.begin(); site_i != end; ++site_i) {
          if (!(*site_i).second->wasVisited())
             if (numOfConnectedComponents >= 1) // (1)
                throw logic_error("Não é um grafo conexo");
    
          dfsRcheck((*site_i).second);
          numOfConnectedComponents++;
       }
    }
    
    // Depth-first search.
    void Space::dfsRcheck(spSite site) {
       site->setVisited(true);
    	
       wpSite_map::iterator adjSite_i = site->beginAdjacentSites();
       wpSite_map::iterator end = site->endAdjacentSites();
       for (; adjSite_i != end; ++adjSite_i)
          if (!((*adjSite_i).second).lock()->wasVisited())
             dfsRcheck(((*adjSite_i).second).lock());
    }
    

    O código apresentado é o algoritmo de busca em profundidade (com o grafo sendo representado por uma lista de adjacência). O método checkGraph() conta quantas componentes conexas o Space possui. Sabemos que um grafo conexo possui somente uma componente conexa, portanto só precisamos detectar se o Space tem mais de uma componente para saber que não obedece à definição, como mostra a linha (1) [4]. Não há um motivo especial para ter usado a busca-em-profundidade, a busca-em-largura também funcionaria desde que os nós fossem marcados como visitados.

    Não explicarei a implementação de todas as classes neste texto. Porém alguns aspectos das classes Agent e Site deverão ser detalhados para um melhor entendimento do restante da seção quando o mecanismo das ações será esclarecida.

    A classe Agent

    O código abaixo mostra o trecho de código que será comentado. Trata-se de um conjunto de campos privados, cada um com seus respectivos métodos get's e set's que foram ocultados assim como o restante da classe.

    
    class Agent {
    
    (...)
    
    private:
       wpAgent          self;
       
       pthread_mutex_t  mutex;
       pthread_cond_t   cond;
       
       unsigned long    turn;
       
       bool             finish_thread;
       
       spAction         action;
       spAction         possibleAction;
       bool             action_performed;
       bool             action_accepted;
       
       
       spActionInfo_set deniedActions;
    
    (...)
    
    };
    

    O campo self é uma auto-referência. Ele faz o papel do this em conjunto com os apontadores inteligentes. Este campo está presente em quase todos as classes.

    Os campos mutex e cond são usados para fazer a sincronização entre as threads. A classe MMASS (que representa todo ambiente) também possui esses campos. O uso detalhado deles será visto no mecanismo de gerenciamento das ações, agora só precisamos notar que cada instância de Agent tem seu próprio mutex e variável de condição.

    O turn também é encontrado na classe MMASS e nas threads de agente. A sincronização do sistema é feito através do campo turn das três estruturas. Aqui é importante notar que cada objeto Agent tem seu próprio turn e cada thread de agente também tem seu próprio turn.

    A flag finish_thread, também encontrada na classe MMASS, indica que a thread do agente (e do ambiente no caso do MMASS) pode ser finalizada com segurança.

    Antes de comentar os outros campos, é necessário conhecer um pouco a finalidade de cada thread. Basicamente, as threads de agente faz com que o objeto Agent correspondente escolha uma ação, e a thread do ambiente faz o gerenciamento das ações escolhidas por cada um dos Agent's. A thread do agente, após o Agent correspondente escolher uma ação, entra em estado de espera e só é sinalizada pela thread do ambiente depois que esta ação for gerenciada, nesse instante já é possível saber se ela foi aceita ou não.

    Quando um Agent escolhe uma ação, ela é atribuída ao campo possibleAction, este campo guarda uma ação que ainda não foi gerenciada pelo ambiente. O campo action guarda uma ação que foi aceita pelo ambiente (e portanto já passou pela fase de gerenciamento). Então, se o estado do campo action for action != NULL, então sabemos que o Agent teve uma ação aceita, mas isso não é suficiente para cobrir outros casos, por isso esta classe tem mais duas flags auxiliares.

    A flag action_performed é verdadeira se a ação escolhida pelo agente no turno atual foi gerenciada, caso contrário ela é falsa. A flag action_accepted é verdadeira se a última ação que foi gerenciada deste Agent foi aceita (não importa se foi do turno anterior ou do turno atual), caso contrário é falsa. Com essas flags e os campos de ação, conseguimos saber qual a situação do agente em relação à ação escolhida (se já foi gerenciada e se foi aceita). O uso deles serão vistos mais adiante no mecanismo de gerenciamento das ações.

    Para as classes Action, Agent e Site, existem classes correspondentes ActionInfo, AgentInfo e SiteInfo. Estas classes guardam informações das classes originais na forma de strings. A finalidade delas é impedir com que a classe Agent tenha acesso direto a estruturas que não podem manipular diretamente (por exemplo, outros Agent's). O campo deniedActions é um conjunto de ActionInfo's das ações que não foram aceitas pelo ambiente no turno corrente.

    A classe Site

    Segue abaixo o trecho de código a ser comentado.

    
    class Site {
    
    (...)
    
    private:
       wpSpace                          space;
       
       map<const string, unsigned long> sitesDistances;
       queue<const string>              sitesInDistanceOrder;
       
    
    (...)
    
    };
    

    O campo sitesDistances é um mapa onde a chave é o nome de um Site e o valor é a distância entre o este objeto (a própria instância de Site) e o Site cujo o nome é a chave. Ou seja, é um container que guarda as distancias entre este Site e os outros Site's da mesma camada.

    O campo space é uma referência para o espaço onde este Site está contido (definição 2).

    O campo sitesInDistanceOrder é uma fila FIFO que contém os nomes dos outros Site's em ordem crescente. Na verdade ele é um pouco mais do que isso, explicarei melhor a seguir.

    A definição de distância entre dois sítios é o mesmo de grafos, isto é, o tamanho do menor caminho entre ambos os sítios. Portanto uma busca-em-largura resolve o problema do calculo das distâncias. Abaixo está o código que atribui os valores para os campos apresentados acima.

    
    void Site::calculateDistances() {
       const string name;
       unsigned long distance;
       
       // Breath-search-first
       queue<wpSite> sitesQueue;
       sitesQueue.push(this->self);
    
       while (!sitesQueue.empty()) {
          spSite site = sitesQueue.front().lock();
          sitesQueue.pop();
          distance = this->sitesDistances[site->getName()];
    
          wpSite_map::const_iterator site_i, s_end;
          site_i = site->adjSites.begin();
          s_end = site->adjSites.end();
          
          for (; site_i != s_end; ++site_i) {
             name = (*site_i).second.lock()->getName();
             
             if ((name != this->name) &&
                 (this->sitesDistances[name] == 0)) {
                   sitesQueue.push((*site_i).second);
                   this->sitesDistances[name] = distance + 1;
                   this->sitesInDistanceOrder.push(name);
             }
          }
       }
    }
    

    O código acima é uma busca-em-largura. Durante a busca, as distâncias são calculadas e os campos comentados anteriormente são construídos. Em especial, note que o sitesInDistanceOrder é preenchido na mesma ordem da busca. Lembre-se que este campo é uma fila FIFO e portanto iteramos sobre seus elementos na ordem em que foram inseridos, portanto, por construção, esse campo guarda uma árvore geradora mínima do espaço (ou do grafo conexo), com este objeto (o próprio Site) sendo a raiz (porém, note também que a raiz não é incluso na fila).

    Gerenciamento das ações

    A seguir será apresentado o mecanismo de gerenciamento das ações. Este mecanismo é composto por várias threads e estas tthreads são sincronizadas pelo campo turn das classes Agent, Site e da thread de agente.

    Threads dos agentes

    Cada agente possui uma thread correspondente, seu código é apresentado e explicado a seguir. Esta thread é responsável pelo gerenciamento das ações do agente e pela sincronização com o ambiente, que também possui sua thread.

    Em geral, as funções da biblioteca Pthread podem ser ignoradas, sua função é evitar que as threads executem seções críticas simultaneamente e portanto não fazem parte do mecanismo propriamente dito, exceto as funções de espera e de sinalização que tem papel importante. Para ser mais direto, pode ignorar as funções pthread_mutex_lock e pthread_mutex_unlock, mas não ignore as funções pthread_cond_wait e pthread_cond_signal/broadcast.

    
    void* MMASS::_agentBehaviourThread(void *arg) { 
       spAgent agent = *((spAgent*)arg);
       unsigned long turn = 0;
       spContext localContext;
       spAction nullAction;
    
       pthread_mutex_lock(&agent->getMutex());
       while (!agent->finishThread()) {
          pthread_mutex_unlock(&agent->getMutex());
    
          localContext = agent->getSite()->sense(agent, turn); // (1)
    		
          pthread_mutex_lock(&agent->getMutex());
          if ((!agent->actionPerformed()) &&
             (agent->getPossibleAction() == nullAction) &&
             (agent->getAction() == nullAction))
                agent->actionSelect(localContext); // (2)
          pthread_mutex_unlock(&agent->getMutex());
    		
          bool outcome = agent->getSite()->act(agent, turn); // (3)
    
          pthread_mutex_lock(&agent->getMutex());
          if (outcome != false) {
             agent->setActionPerformed(false);
             agent->clearDeniedActions();
             turn++;
          }
       }
       pthread_mutex_unlock(&agent->getMutex());
    
       pthread_exit(NULL);
    }
    

    Para fins comparação e esclarecimento de alguma dúvida sobre o código acima, também vou mostrar o pseudo-código do artigo do Vizzari abaixo.

    Lord of the rings

    Dentro do while temos os três passos que são tomados por uma thread de agente. Primeiro, toma-se conhecimento do estado local do ambiente, esta informação encontra-se dentro do objeto localContext (linha (1)). Neste objeto podemos obter os nomes dos agentes vizinhos, seus tipos, os nomes dos sítios nos quais estes agentes estão posicionados, os nomes de todos os sítios vizinhos e os campos ativos no local e na vizinhança. Todas estas informações são necessárias para escolher alguma ação de acordo com as definições 10 à 13, além disso, este objeto garante que o Agent não terá acesso direto a outros Agent. Em seguida, baseado no contexto local, o agente escolhe qual ação tomar (linha (2)). Finalmente, o ambiente é notificado da escolha, e a thread do agente espera o ambiente responder se aceitou a ação (linha (3)), caso positivo a thread avança seu turno, caso contrário repetimos os 3 passos (escolhendo outra ação na linha (2)).

    Há um caso onde não é esta thread que faz com que seu objeto Agent correspondente escolha uma ação. Lembre-se que uma reação só é aceita se todos concordarem em realizá-la, e então cada um dos envolvidos escolhe sua própria reação. Quando isso ocorre não é a thread do agente que faz os envolvidos escolher a reação e sim a thread do ambiente (que veremos mais adiante). Aqui é importante perceber que a thread do agente não faz parte do agente, esta thread é apenas um mecanismo para manipular os objetos Agent's. Repare que a ação só é escolhida se o if acima da linha (2) for satisfeita. As condições deste if servem para cobrir o caso da reação que acabou de ser descrito.

    Para explicá as condições é mais fácil pensar na negação: quando que o if não é satisfeito? Quando o possibleAction != NULL significando que o agente já escolheu uma ação e deve esperar para ser gerenciado, quando action != NULL, ou seja, uma ação já foi escolhida, gerenciada e acieta e o agente deve esperar ela ser executada, ou quando action_performed == true que ocorre quando uma ação já foi escolhida, gerenciada, aceita e concretizada. Ou seja, um agente só escolhe uma ação se ainda não escolheu uma para o turno atual.

    Cada thread manipula seu próprio objeto do tipo Agent, ou seja, ela lê e escreve em seu próprio espaço da memória. Neste sentido podemos enxergar as threads dos agentes e a thread do ambiente como um problema parecido com o do tipo produtor / consumidor, onde a thread do agente produz uma ação em seu buffer (o objeto Agent) e espera pelo ambiente consumí-lo. Esta espera ocorre na linha (1), e a produção ocorre na linha (2). Na linha (1) também ocorre a sincronização dos turnos, isto é, ocorre a espera pelo turno das duas threads (do agente e do ambiente) serem as mesmas.

    Note que o MMASS é um modelo de troca indireta de informações e o código acima torna isto explícito. Para realizar uma ação o agente envia uma mensagem para o sítio em que se encontra (linha (3)), e então ele escolhe sua ação baseado nas informações do objeto localContext, que é um conjunto de strings (linha (2)), e portanto, não há acesso direto a outros agentes, ou seja, o agente nunca manda uma mensagem diretamente para outro, o ambiente é sempre um intermediário (linhas (1) e (3)).

    Até aqui já é o suficiente para entender esta thread e é neste ponto que o Vizzari encerra a explicação para o caso multithread sincronizado. Porém, como este trabalho é uma implementação, irei aprofundar um pouco mais a explicação. Segue abaixo o código do método sense().

    	
    spContext Site::sense(spAgent agent, unsigned long turn_) {
       spMMASS mmass = this->space.lock()->getMASS()->getMMASS();
    
       pthread_mutex_lock(&mmass->getMutex());
       pthread_mutex_lock(&agent->getMutex());
       while ((turn_ > mmass->getTurn()) && (!agent->finishThread())) {//(1)
          pthread_mutex_unlock(&agent->getMutex());
          pthread_cond_wait(&mmass->getCond(), &mmass->getMutex());
          pthread_mutex_lock(&agent->getMutex());
       }
    
       spContext context context = agent->getSite()->createContext();
       pthread_mutex_unlock(&agent->getMutex());
       pthread_mutex_unlock(&mmass->getMutex());
    
       return context;
    }
    

    Este método recebe um Agent e o turno da thread deste agente como parâmetros e retorna o contexto do Site onde o Agent se encontra. Veja na linha (1) que este método faz com que a thread do agente entre em estado de espera se o turno dela for maior que o turno do ambiente, portanto o contexto só é criado se o turno do agente for igual ou menor ao turno do ambiente. Quando o turno do ambiente é maior (e portanto diferente) que o do agente, o contexto criado fica fora de sincronia, ou seja, o agente irá receber um contexto de um turno diferente ao esperado por ele. Apesar dessa inconsistência neste local do código, toda ação baseada em contexto errado é descartada.

    Há um detalhe importante neste método, ele faz com que uma thread adquira dois mutex ao mesmo tempo. Isto é perigoso pois pode provocar deadlock, para evitar isso é necessário que todas as threads adquiram os mutexes na mesma ordem. No caso da minha implementação a ordem é: primeiro o mutex do MMASS e depois o mutex do Agent.

    O método actionSelect é definido pelo usuário do MMASS e será explicado em outra seção. O código do método act segue abaixo.

    
    bool Site::act(spAgent agent, unsigned long turn_) {
       return this->space.lock()->getMASS()->getMMASS()->act(agent, turn_);
    }
    
    bool CMMASS::act(spAgent agent, unsigned long turn_) {
       bool ret = false;
       spAction nullAction;
    
       pthread_mutex_lock(&agent->getMutex());
       while ((!agent->finishThread()) && // (1)
              (this->turn <= turn_) &&
              (!agent->actionPerformed()) &&
              (agent->getPossibleAction() != nullAction) &&
              (agent->getAction() == nullAction))
                 pthread_cond_wait(&agent->getCond(), &agent->getMutex());
    
       if ((this->turn > turn_) || // (2)
           (agent->getAction() != nullAction) ||
           (agent->actionAccepted())) {
              agent->getPossibleAction().reset();
              ret = true;
       }
       pthread_mutex_unlock(&agent->getMutex());
    
       return ret;
    }
    

    Este método recebe um Agent e o turno da thread deste agente e retorna true caso a ação escolhida pelo Agent para este turno foi aceita ou false caso contrário. A função deste método é fazer a thread do agente esperar pela ação ser gerenciada. Um detalhe a ser notado é que this->turn refere-se ao turno do ambiente e turn_ é o turno da thread do agente.

    Na linha (1) temos as condições de espera, é mais fácil entender estas condições explicando suas negações (quando que a thread não espera). O caso possibleAction == NULL ou action != NULL significa que a ação foi gerenciada e ainda não foi executada, se a ela tivesse sido executada a thread do ambiente destruiria o action e teriamos que action == NULL, por isso a thread do ambiente também liga a flag actionPerformed que indica que a ação para o turno corrente foi gerenciada (independentemente se foi aceita ou não). Portanto, a thread do agente entra em estado de espera se a ação ainda não foi gerenciada.

    Na linha (2), depois que a ação foi gerenciada examinamos o estado de alguns campos para saber se a ação foi aceita. Se action != NULL então a ação foi aceita e não foi executada, caso a ação tenha sido aceita e executada então teriamos que action == NULL, por isso a flag actionAccepted é ligada e indica que a última ação escolhida foi aceita. Nesses casos o método retornará true, caso contrário false.

    Note que tanto na linha (1) quanto na linha (2) as condições que envolvem o turno não foram explicadas. Elas cuidam do caso que a thread do agente está fora de sincronia com a thread do ambiente. Caso a thread do agente esteja atrasada (seu turno é menor que o turno do ambiente), isto quer dizer que nesses turnos que se passaram foi a thread do ambiente que fez com que o objeto Agent correspondente escolhesse as ações (isso só é possível no caso de reações). E portanto, qualquer ação escolhida para um turno que já se passou deve ser ignorada (este é o mesmo caso do contexto inconsistente que já foi comentada nesta seção).

    Thread do ambiente

    Código da thread do ambiente é apresentado abaixo:

    
    void* MMASS::_environmentBehaviourThread(void *arg) {
       ThreadArgument *argument = (ThreadArgument*)arg;
       spAgent_set agents = argument->agents;
       spMMASS mmass = argument->mmass;
       bool actAccepted;
       unsigned long turn;
    
       while (true) {
          turn = mmass->getTurn();
          spAgent_set agentsChoosingAction = agents; // (1)
    
          while (!agentsChoosingAction.empty()) { // (2)
             spAgent_set::iterator agent_i = agentsChoosingAction.begin(),
                a_end = agentsChoosingAction.end();
    
             while (agent_i != a_end) {
                pthread_mutex_lock(&(*agent_i)->getMutex());
                spAction action = (*agent_i)->collectAction(turn);
    
                if ((*agent_i)->getTurn() >= turn) {
                   pthread_mutex_unlock(&(*agent_i)->getMutex());
                   actAccepted = _manage((*agent_i), action, turn); // (3)
    
                   pthread_mutex_lock(&(*agent_i)->getMutex());
                   if (actAccepted) {
                      pthread_mutex_unlock(&(*agent_i)->getMutex());
                      agent_i = agentsChoosingAction.erase(agent_i); // (4)
                      continue;
                   }
                }
                pthread_mutex_unlock(&(*agent_i)->getMutex());
                ++agent_i;
             }
          }
    
          pthread_mutex_lock(&mmass->getMutex());
          _clearFields(mmass); // (5)
          _performActions(agents); // (6)
          _clearOccupiedSites(mmass); // (7)
          mmass->nextTurn(); // (8)
          pthread_cond_broadcast(&mmass->getCond());
          pthread_mutex_unlock(&mmass->getMutex());
    
          if (mmass->finishThread()) break;
       }
    
       delete (ThreadArgument*)arg;
       pthread_exit(NULL);
       return NULL;
    }
    

    Segue abaixo o pseudo-código do Vizzari abaixo. Há uma diferença na minha implementação e a do pseudo-código. Atribuir true para a flag action_performed é feito dentro da função manage, e atribuir false para a flag é feito na thread do agente. Apesar dessa diferença, a função da thread do ambiente permanece inalterada.

    Thread do ambiente

    Na linha (1) uma fila é inicializada com todos os agentes do sistema. Esta estrutura é a fila de todos os agentes que estão escolhendo alguma ação para o turno atual (acima da linha (1)). Um agente sai desta fila somente quando sua ação é aceita pela thread do ambiente (linha (4)). O ambiente só avança de turno quando todos os agentes tiveram suas ações para o turno atual aceitas (linha (2) e (8)).

    A parte principal desta thread é a linha (3). A função manage é responsável por gerenciar adequadamente cada tipo de ação, por exemplo, chamando a função reactionManager para gerenciar reações. A função manage também acorda as threads de agente que estavam esperando pelo ambiente aceitar ou não uma ação (lembre-se que esta espera ocorre na função act do código anterior).

    Apesar das ações serem aceitas na linha (3), elas não são imediatamente executadas. Há dois motivos para isso ocorrer. Primeiro, devemos preservar o estado do ambiente para o turno em andamento, pois os agentes que ainda não escolheram uma ação devem observar o contexto do turno atual. Segundo, porque os agentes que já escolheram e tiveram suas ações confirmadas podem ter que reconsiderá-las, por exemplo, no caso da reação, se um agente A escolheu e teve confirmada uma ação de emitir e um agente B pede uma reação com o agente A, talvez este descarte a emissão para reagir com o agente B.

    Portanto as ações são executadas assim que todos os agentes tiverem suas ações aceitas, ou seja, somente quando é possível avançar de turno. Isso quer dizer que o estado do ambiente e dos agentes só mudam (em consequência das ações) no final de cada turno (na linha (6)).

    O Vizzari encerra as explicações para o caso multithread e síncrono neste ponto, mas como já foi dito, continuo a explicação por se tratar de uma implementação concreta.

    A linha (5) destrói todos os campos ativos antes de executar as ações. Isto quer dizer que os campos não persistem no ambiente com o tempo, ou seja, se um agente decide emitir um campo no turno n, os efeitos deste campo poderão ser percebidos no turno n + 1 e desaparecerão no turno n + 2.

    Todo MASS (ou camada) possui um set de sítios ocupados, ela é usada pelo transportManagement, que será explicada mais adiante. Na linha (7) esta estrutura é esvaziada em todas as camadas.

    Note que após o avanço do turno, todas as threads que estavam esperando na variável de condição do ambiente são acordadas. Lembre-se que as threads de agente esperam nesta variável quando entram na função sense (do código anterior) e esperam pela sincronização dos turnos.

    Segue abaixo o código do manage.

    
    bool MMASS::_manage(spAgent agent,
       spAction action, unsigned long turn) {
       spAction nullAction;
       bool ret = true;
     
       pthread_mutex_lock(&agent->getMutex());
       if (agent->getAction() != nullAction) { // (1)
             pthread_cond_signal(&agent->getCond());
             pthread_mutex_unlock(&agent->getMutex());
             return true;
       }
       pthread_mutex_unlock(&agent->getMutex());
    
       if (action == nullAction) return false; // (2)
    
       ret = action->manage(agent, action, turn); // (3)
    
       pthread_mutex_lock(&agent->getMutex());
       pthread_cond_signal(&agent->getCond()); // (4)
       pthread_mutex_unlock(&agent->getMutex());
    
       return ret;
    }
    

    A função recebe um Agent e sua Action escolhida para o turno turn, retorna true se a ação foi aceita e false caso contrário. Em dois casos a ação não é gerenciada: quando ela já foi gerenciada e aceita (linha (1), a condição action != NULL só é verdade se o agente já teve sua ação gerenciada e aceita) ou quando ainda não foi escolhida uma ação (linha (2)). Em qualquer outro caso a ação será gerenciada na linha (3), nesta linha a função adequada para a ação será chamada.

    Depois que a ação é gerenciada, a thread de agente que estava esperando por isto é acordada (linha (4)). Lembre-se que uma thread de agente espera uma ação ser gerenciada na função act.

    Agora vamos ver as funções de gerenciamento específicos para cada tipo de ação, começando pelo triggerManagement para a ação acionar, e o emitManagement para a ação emitir. Em seguida veremos o reactionManagement para reação e o transportManagement para caminhar. Todas essas funções recebem os mesmos parâmetros da função manage.

    
    bool MMASS::_triggerManagement(spAgent agent,
       spAction action, unsigned long turn) {
       pthread_mutex_lock(&agent->getMutex());
       agent->performAction(); // (1)
       agent->setActionAccepted(true);
       agent->setActionPerformed(true);
       pthread_mutex_unlock(&agent->getMutex());
       return true;
    }
    
    bool MMASS::_emitManagement(spAgent agent,
       spAction action, unsigned long turn) {
       pthread_mutex_lock(&agent->getMutex());
       agent->performAction(); // (1)
       agent->setActionAccepted(true);
       agent->setActionPerformed(true);
       pthread_mutex_unlock(&agent->getMutex());
       return true;
    }
    

    Essas duas funções são as mais simples das quatro. Elas são idênticas pelo fato das ações acionar e emitir nunca serem negadas, então o gerenciamento delas resume-se a mudar o estado dos campos do objeto Agent para indicar que a ação foi aceita. Na linha (1), o método peformAction é chamada sempre que uma ação é aceita, nela é atribuido ao campo action a referência à ação que foi aceita e o campo possible_action recebe o valor NULL. Nas duas linhas seguidas mudamos as flags action_performed para true, pois a ação foi gerenciada, e a flag action_accepted para true.

    Sempre que uma ação é aceita, os passos a serem tomados são parecidos. Portanto essas funções podem ser tomados como um modelo do que deve ser feito para aprovar uma ação, com algumas pequenas modificações dependendo da ação.

    A seguir vamos ver o transportManagement que faz um agente caminhar do seu sítio atual (origem) para algum sítio vizinho (destino).

    
    bool MMASS::_transportManagement(spAgent agent,
       spAction action, unsigned long turn) {
       spMASS mass;
       set<const string> occupiedSites;
       set<const string>::iterator site;
       const string currentSite;
       const string destinationSite;
       
       mass = agent->getMASS();
       occupiedSites = mass->getOccupiedSites(); // (1)
       currentSite = agent->getSite()->getName();
       destinationSite = action->getDestinationSiteName();
    
       site = occupiedSites.find(destinationSite); // (2)
       if (site != occupiedSites.end()) { // (3)
          pthread_mutex_lock(&agent->getMutex());
          agent->addDeniedAction(action->createActionInfo());
          agent->notifyFailure(turn); // (4)
          agent->setActionAccepted(false);
          agent->setActionPerformed(true);
          pthread_mutex_unlock(&agent->getMutex());
          return false;
       }
    
       pthread_mutex_lock(&agent->getMutex());
       agent->performAction(); // (5)
       agent->setActionAccepted(true);
       agent->setActionPerformed(true);
       mass->addOccupiedSite(destinationSite); // (6)
       pthread_mutex_unlock(&agent->getMutex());
       return true;
    }
    

    Na linha (1) o set de sítios ocupados é inicializado, esta é a mesma estrutura que é esvaziada no final do turno no environmentBehaviourThread. Na primeira vez (em um dado turno) que este set é inicializado, ele possui apenas os sítios onde os agentes estão posicionados.

    Em seguida, na linha (2) e (3), verificamos se o sítio de destino (pra onde o agente vai se mover) está ocupado, caso esteja ocupado, a ação de caminhar deve ser negada, para isso o if da linha (3) é executado. O método notifyFailure da linha (4) é o oposto de performAction da linha (5). O notifyFailure é chamado sempre que uma ação é negada, nele o campo possible_action é destruído sem ocorrer nenhuma troca. Nas duas linhas seguintes, mudamos os estados das flags para refletir no gerenciamento (action_performed = true) e na negação da ação (action_accepted = false). Veja também que acima da linha (4), a ação é colocada no conjunto de ações negadas para o agente no turno corrente.

    Se a ação for aceita então os passos tomados são os mesmos do triggerManagement (onde a ação é sempre aceita) como podemos ver na linha (5) em diante, exceto a linha (6) onde adicionamos o sítio de destino no set de sítios ocupados do turno corrente. Como foi dito, a primeira vez que este set é inicializado temos somente os sítios onde os agentes estão posicionados no turno atual, nas consultas seguintes esta estrutura também terá os sítios que serão ocupados no turno seguinte. Isto é feito para evitar que dois Agent's consigam mover para um mesmo Site ao mesmo tempo. Nesta implementação, o agente que caminhar primeiro para o sítio conseguirá concretizar a ação.

    Agora veremos o código do reactionManagement, o mais complexo dos quatro.

    
    bool MMASS::_reactionManagement(spAgent agent,
       spAction action, unsigned long turn) {
       spMASS mass = agent->getSite()->getSpace()->getMASS();
       bool agreed = true;
       spAgentInfo_set involvedAgents = action->getReactionPartners();
       spAgent_set reactingAgents;
       reactingAgents.insert(agent);
    
       spAgentInfo_set::iterator info_i, i_end = involvedAgents.end();
       for (info_i = involvedAgents.begin(); info_i != i_end; ++info_i) {
          spAgent agent = mass->getAgent((*info_i)->getAgentName());
          pthread_mutex_lock(&agent->getMutex());
          
          if (!agent->agreeReaction(involvedAgents)) { // (1)
             pthread_mutex_unlock(&agent->getMutex());
             agreed = false;
             break;
          }
          pthread_mutex_unlock(&agent->getMutex());
          reactingAgents.insert(agent);
       }
    
       spAgent_set::iterator agent_i, a_end = reactingAgents.end();
       agent_i = reactingAgents.begin();
       if (agreed) { // (2)
          spAction nullAction;
          
          for (; agent_i != a_end; ++agent_i) {
             pthread_mutex_lock(&(*agent_i)->getMutex());
             
             if ((*agent_i)->getAction() == nullAction) // (3)
                (*agent_i)->setActionPerformed(true);
             else {
                spAction act = (*agent_i)->getAction();
                
                if (act->getIdentifier() == "Transport") { // (4)
                   const string destSite;
                   destSite = act->getDestinationSiteName();
                   mass->eraseOccupiedSite(destSite);
                }
             }
             (*agent_i)->setActionAccepted(true);
             (*agent_i)->performAction();
             pthread_mutex_unlock(&(*agent_i)->getMutex());
          }
       } else { // (5)
       
          for (; agent_i != a_end; ++agent_i) {
             pthread_mutex_lock(&(*agent_i)->getMutex());
             (*agent_i)->setActionAccepted(false);
             (*agent_i)->addDeniedAction(action->createActionInfo());
             (*agent_i)->notifyFailure(turn);
             pthread_cond_signal(&(*agent_i)->getCond());
             pthread_mutex_unlock(&(*agent_i)->getMutex());
          }
          pthread_mutex_lock(&agent->getMutex());
          agent->setActionPerformed(true); // (6)
          pthread_mutex_unlock(&agent->getMutex());
          return false;
       }
    
       return true;
    }
    

    O Vizzari fornece o pseudo-código deste método, mostrado na figura abaixo.

    Thread do ambiente

    A primeira parte desta função (o primeiro laço for), na linha (1), pergunta para cada agente envolvido se ele concorda em participar da ação, caso algum não aceite participar então já sabemos que a reação não será realizada, portanto não é necessário perguntar ao restante dos agentes, em seguida é atribuído o valor falso para a variável agreed que indica se todos os agentes concordaram ou não com a reação.

    Depois que o primeiro laço acaba, o agreed é verificado para saber se a ação foi aceita (linha (2)) ou não (linha (5)). Caso tenha sido aceita, as passos tomados são parecidos com os da função emitManagement (onde as ações são sempre aceitas), as flags e alguns campos de cada Agent envolvido devem mudar de estado para refletir o fato da reação ter sido aceita. Na linha (3) é verificado se os agentes já tiveram alguma ação aceita neste turno, se não tiveram então a flag action_performed é ligada, caso contrário esta flag já está ligada e não é preciso modificá-la, porém, no caso da ação anterior ter sido do tipo transport (caminhar) então o conjunto de sítios ocupados deve ser atualizado para refletir o cancelamento (linha (4) em diante). Depois disso, a flag action_accepted é ligada e os valores dos campos possible_action e action são trocados através do método performAction, igual nas funções de gerenciamento anteriores quando a ação é aceita.

    Caso a reação não tenha sido aceita, então é executada linha (5) em diante. Dentro do for é feito operações sobre os agentes que aceitaram realizar a reação que foi negada. Para cada um desses agentes é atribuído o valor falso para o action_accepted, a flag action_performed permanece no estado que estava: verdadeiro para os que já tinham uma ação gerenciada e falso para os que não tinham, exceto para o autor da reação onde, na linha (6), essa flag deve ser ligada. Em seguida, a reação recusada é colocada no conjunto de ações negadas para cada agente e então o possible_action é destruído pelo método notifyFailure e por fim cada um dos agentes são acordados.

    Esta seção encerra aqui. Não pretendo descrever a implementação do sistema todo neste texto. As definições formais e o mecanismo das ações explicados já são o suficiente para entender como utilizar esta implementação do MMASS para a definição de sistemas multiagentes, este é o assunto da seção a seguir.


    Como utilizar [índice]

    Será mais fácil explicar o uso deste MMASS com o auxílio de um exemplo. Nesta seção será definido um sistema com dois agentes, um gato e um rato. O primeiro o comportamento dos agentes devem ser determinados: o gato deve perseguir o rato, o rato deve fugir do gato, se o gato alcançar o rato então o rato é capturado, se o gato não percebe a presença do rato então ele permanece parado, se o rato não percebe a presença do gato então ele permanece parado. Agora que o comportamento do sistema está claro, vamos usar o MMASS para implementá-lo.

    A implementação é dividido em duas fases. A primeira é a definição de classes e métodos, a segunda parte é a construção do cenário, ou seja, montar as camadas e posicionar os agentes. Na primeira fase, para saber quais classes implementar, reveja o diagrama UML do framework.

    Figura 2

    Com exceção da classe Action, todas as outras classes abstratas serão estendidas (Field, Agent, State, Trigger e Reaction).

    No exemplo, há dois tipos de agentes, o gato e o rato, e portanto devem ser criadas duas classes do tipo Agent. Cada classe Agent deve ter sua própria classe State correspondente. Então o primeiro passo é estender a classe State. Segue o cabeçalho dela abaixo.

    
    class State {
    public:
       State();
       virtual ~State() = 0;
    };
    

    Esta classe é bem simples pois ela é inteiramente dependente da aplicação, inclusive a sua interface. Abaixo está o código dos estados do gato e do rato.

    
    class CatState : public State {
    public:
       CatState() { mouseCaught = false; }
       ~CatState() { }
    
       void setMouseCaught(bool b) { mouseCaught = b; }
       bool getMouseCaught() { return mouseCaught; }
       
    protected:
       bool mousecaught;
    };
    
    class MouseState : public State {
    public:
       MouseState() { catCaughtMe = false; }
       ~MouseState() { }
    
       void setCatCaughtMe(bool b) { catCaughtMe = b; }
       bool getCatCaughtMe() { return catCaughtMe; }
       
    protected:
       bool catCaughtMe;
    };
    

    Uma vez definido os estados, é possível definir os agentes. Segue abaixo o cabeçalho da classe Agent.

    
    class Agent {
    
       (...)
       
    public:
       class AgentFactory {
          virtual Agent* create(const string &name_) const = 0;
          friend Agent;
       };
    
       virtual ~Agent() = 0;
       virtual const string getTypeName() = 0;
       
       (...)
       
    protected:
       Agent(const string &name_);
       
       (...)
       		
    private:
       static spAgent createAgent(AgentFactory &factory, const string &name)
       {
          Agent *agent = factory.create(name);
          spAgent agent_(agent);
          agent_->setSelf(agent_);
    
          return agent_;
       }
       
       virtual void actionSelect(spContext localContext) = 0;
       virtual const bool agreeReaction(spAgentInfo_set &involvedAgents) = 0;
       
       (...)
       
    };
    
    template<class StateClass>
    class CAgent : public Agent {
    public:
       boost::shared_ptr<StateClass> getState() { return state; }
    
    protected:
       boost::shared_ptr<StateClass> state;
    
       CAgent(const std::string &name_, boost::shared_ptr<StateClass> state_)
          : Agent(name_), state(state_) { }
    };
    

    A partir de um objeto Agent deve ser possível recuperar o seu correspondente objeto State específico (ou estendido). Uma solução poderia ser a criação de um método getState que devolve uma referência para a classe State e então o usuário faz o downcast manualmente para a classe derivada, o downcast geralmente é necessário para ter acesso à interface da classe estendida. Outra solução pode ser a que foi usada pela classe CAgent que usa templates para guardar e retornar a referência do State específico, sem a necessidade de fazer um cast. Então, para definir os agentes do sistema, a classe CAgent deve ser expandida.

    Repare que não é possível instanciar diretamente as classes derivadas de Agent, os dois métodos usados para isso (o construtor e o createAgent) não são públicos. A criação de objetos dessas classes é feita pela classe MASS (a camada), como será visto mais adiante.

    O método createAgent recebe como um de seus parâmetros o AgentFactory específico do agente a ser instanciado. Isto é necessário pois este método não tem acesso direto aos construtores das classes derivadas, este acesso é feito através do método create da classe AgentFactory.

    Abaixo está o código do agente gato. Note que o estado correspondente (CatState) é passado como parâmetro ao template. A classe do agente rato é análogo e por isso não será mostrado.

    
    class CatAgent : public CAgent<CatState> {
    public:
       class CatAgentFactory : public Agent::AgentFactory {
          Agent* create(const string &name_) const {
             boost::shared_ptr<CatState> state(new CatState);
             return new CatAgent(name_, state);
          }
       };
    
       ~CatAgent() { }
       const string getTypeName() { return "CatAgent"; }
       
    protected:
       CatAgent(const string &name_) : CAgent(name_) {
          // Revelado mais adiante!
       }
       		
    private:
       void actionSelect(spContext localContext) {
          // Revelado mais adiante!
       }
       
       const bool agreeReaction(spAgentInfo_set &involvedAgents) {
          // Revelado mais adiante!
       }
    };
    

    Pela definição formal de agente, este é composto pelo valor do estado (neste caso, o CatState assume valores verdadeiro ou falso), pelo sítio (será visto mais tarde na construção da camada) e pelo tipo do agente que é representado pelo método getTypeName. Na verdade o tipo do agente não é somente uma thread, uma explicação melhor sobre isso será dado logo mais.

    Agora é hora de definir os campos. Abaixo está o cabeçalho da classe Field.

    
    class Field {
    public:
       virtual ~Field() = 0;
    
       virtual spField createField(double value_) = 0;
       
       virtual double diffusion(spSite source,
                                double generatedValue, spSite destiny) = 0;
       virtual double compose(double value1, double value2) = 0;
       virtual bool compare(double value1, double value2) = 0;
    
       virtual const string getIdentifier() = 0;
    
       double valuePerceivedByAgent(spAgent agent) {
          // Revelado mais adiante!
       }
       
       (...)
    
    protected:
       Field(double value_);
       
       (...)
       
    };
    

    Para conseguir o comportamento desejado aos agentes, o gato emitirá um campo que que espanta o rato, e este emitirá um campo que atrai o gato. Nossa camada terá uma área de 10x10 sítios (100 sítios ao todo), portanto farei os campos difundirem em um raio de 4 sítios de distância. Segue abaixo o código do campo do gato, que espanta o rato. O campo do rato é análogo.

    
    class CatField : public Field {
    public:
       ~CatField() { }
    
       spField createField(double value_) {
          return spField(new CatField(value_));
       }
       
       double diffusion(spSite source,
                        double generatedValue, spSite destiny) {
          unsigned long distance = source->getDistanceOf(destiny);
          
          if (distance == 0) return generatedValue;
          else if (distance > 4) return 0;
          else return (generatedValue / (distance*distance));
          
          return 0;
       }
       
       double compose(double value1, double value2) {
          return value1 + value2;
       }
       
       bool compare(double value1, double value2) {
          return value 1 > value2;
       }
    
       const string getIdentifier() { return "CatField"; }
    
    protected:
       CatField(double value_);
    };
    

    Voltando um pouco para a classe Agent. O tipo do agente não é somente uma string (lembre-se da definição 9), nesta implementação este conceito está integrado na própria classe Agent e no mecanismo de hierarquia de classes da Orientação a Objetos. Na sua definição formal temos a função percepção que é um mapeamento do valor de estado atual para os coeficientes e limites de cada tipo de campo. Este mapeamento deve ser feito pelo usuário manualmente (pois depende da aplicação), e o local mais indicado para isso é no própio construtor.

    Dito isto e tendo as implementações dos campos, os códigos do construtor do CatAgent e do MouseAgent podem ser revelados.

    
    CatAgent::CatAgent(const string &name_) : CAgent(name_) {
       this->setCoefficient("MouseField", 1);
       this->setThreshold("MouseField", 1);
    }
    
    MouseAgent::MouseAgent(const string &name_) : CAgent(name_) {
       this->setCoefficient("CatField", 1);
       this->setThreshold("CatField", 1);
    }
    

    Ainda sobre o tipo de agente, na sua definição foi dito que um agente consegue perceber um campo se e somente se Comparari (ciτ(s) • wfi, tiτ(s)) = True. Baseado nisso, foi criado o método valuePerceivedByAgent que retorna o valor do campo multiplicado pelo coeficiente correspondente se o agente conseguir sentir o campo, ou retorna zero (nulo) caso contrário. Sempre que um agente precisar consultar o valor de um campo, ele usará este método. Abaixo segue o código que foi omitido anteriormente.

    
    double Field::valuePerceivedForAgent(spAgent agent) {
       unsigned long coefficient;
       const double threshold;
       double newValue;
       coefficient = agent->getCoefficient(this->getIdentifier());
       threshold = agent->getThreshold(this->getIdentifier());
       newValue = this->value * coefficient;
    
       if (this->compare(newValue, threshold)) return newValue;
       return 0;
    }
    

    Até agora não definimos nenhuma ação. A classe campo não é uma ação em si, mas é uma parte importante da emissão. Tanto a ação emitir quanto a ação caminhar não precisam ser estendidas. Mas a classe Reaction deverá ser derivada para criar a ação do gato agarrando o rato quando este for alcançado. Antes disso, será mostrado o código da classe Action, ancestral de Reaction.

    
    class Action {
    public:
       virtual ~Action() = 0;
       virtual void performAction(spAgent agent) = 0;
       
       (...)
       
    protected:
       spAgentInfo_set reactionPartners;
       const string    siteName;
       spField         field;
    
       virtual const string getIdentifier() = 0;
    
    private:
       virtual bool manage(spAgent agent,
                           spAction action, unsigned long turn) = 0;
       
       (...)
       
    };
    

    Lembre-se que todo campo de uma classe tem seu respectivo métodos get e alguns tem set. A classe Action é a base para todas as ações do sistema. Segue abaixo a classe Reaction.

    class Reaction : public Action {
    private:
       bool manage(spAgent agent, spAction action, unsigned long turn) {
          return _reactionManagement(agent, action);
       }
    };
    
    template<typename StateClass>
    class CReaction : public Reaction {
    public:
       void performAction(spAgent agent) {
          this->act(agent, AgentClass::extractState(agent));
       }
       
    protected:
       CReaction(spAgentInfo_set &names) : Reaction(names) { }
       
       virtual void act(spAgent agent,
                        boost::shared_ptr<StateClass> state) = 0;
    };
    

    Parar criar uma reação é preciso estender a classe CReaction e implementar o método getIdentifier e o método act que descreve o que esta reação faz. O motivo da criação da classe CReaction é a mesma da existência da classe CAgent, a reação também precisa ter acesso à classe específica do estado (como pode ser visto no método act).

    Abaixo está o código da reação do gato quando alcança o rato. Esta reação muda o estado do gato para refletir sua conquista.

    
    class CatReaction : public CReaction<CatState> {
    public:
       // Este método não é necessário. Fiz para auxiliar.
       static spAction createReaction(spAgentInfo_set &partners) {
          return spAction(new CatReaction(partners));
       }
    
    protected:
       CatReaction(spAgentInfo_set &partners) : CReaction(partners) { }
       const string getIdentifier() { return "CatReaction"; }
    
    private:
       void act(spAgent agent, boost::shared_ptr state) {
          state->setMouseCaught(true);
       }
    };
    

    O rato deve ter uma reação para responder à reação do gato. Segue abaixo o código dela.

    
    class MouseReaction : public CReaction<MouseState> {
    public:
       static spAction createReaction(spAgentInfo_set &partners) {
          return spAction(new MouseReaction(partners));
       }
    
    protected:
       MouseReaction(spAgentInfo_set &partners) : CReaction(partners) { }
       const string getIdentifier() { return "MouseReaction"; }
    
    private:
       void act(spAgent agent, boost::shared_ptr state) {
          state->setCatCaughtMe(true);
       }
    };
    

    Agora o método agreeReaction dos dois agentes podem ser revelado. Este método foi visto na seção da thread do ambiente, ele é usado para perguntar aos agentes se eles aceitam participar de uma dada reação.

    O agente rato nunca quer reagir com um gato e portanto a resposta dada pelo gato não importa, faremos ele sempre responder negativamente para as reações pois ele deve responder algo. No caso do rato, ele é obrigado a responder positivo para a reação do gato, as reações de outros tipos de agentes serão negadas. O código do método para os dois agentes segue abaixo.

    
    const bool CatAgent::agreeReaction(spAgentInfo_set &involvedAgents) {
       return false;
    }
    
    const bool MouseAgent::agreeReaction(spAgentInfo_set &involvedAgents) {
       spAgentInfo_set::iterator agent_i, a_end = involvedAgents.end();
       for (agent_i = involvedAgents.begin(); agent_i != a_end; ++agent_i)
          if ((*agent_i)->getTypeName() == "CatAgent") return true;
          
       return false;
    }
    

    Como sabemos que só existem o gato e o rato neste exemplo, seria mais fácil o rato simplesmente aceitar qualquer reação, mas para ilustrar melhor o uso do MMASS o método foi implementado deste modo.

    Agora que todas as classes necessárias estão definidas, o método selectAction pode ser mostrado. Este método foi visto na seção da thread do agente, ele é usado para os agentes escolher alguma ação baseado no contexto local.

    Neste exemplo, o comportamento dos agentes exige que eles estejam emitindo campo continuamente ao mesmo tempo que caminham, no MMASS isso não é possível, somente uma ação por turno é realizada. Por isso faremos com que dois turnos do MMASS sejam um turno para o rato e para o gato, desse modo é possível realizar as duas ações "ao mesmo tempo". Então, em todo turno par os agentes emitem o campo, em turno impar os agentes caminham ou reagem. Segue abaixo o método do gato.

    
    const bool CatAgent::actionSelect(spContext localContext) {
       double fieldValue, maxValue, localValue;
    
       if ((this->getTurn() % 2) == 0) {
          this->setPossibleAction(
             Emit::createEmit(spField(new CatField(20))));
             
       } else {
          // Procura o rato pela vizinhança.
          spAgentInfo_set reactPartners;
          spAgentInfo_set::iterator agent_i, a_end;
          agent_i = localContext->beginAdjacentAgents();
          a_end = localContext->endAdjacentAgents();
          
          while (agent_i != a_end) {
          
             if ((*agent_i)->getAgentType() == "MouseAgent") {
                reactPartners.insert((*agent_i));
                this->setPossibleAction(
                   CatReaction::createReaction(reactPartners));
                return;
             }
             ++agent_i;
          }
          
          // Não alcançou o rato, corre atrás dele.
          spSiteInfo site = localContext->getSite(); // Sítio local.
          spField field;
          
          // Percebe o campo do rato.
          spField_set::iterator field_i, f_end = site->endFields();
          for (field_i = site->beginFields(); field_i != f_end; ++field_i) {
             if ((*field_i)->getIdentifier() == "MouseField") {
                field = (*field_i);
                break;
             }
          }
          
          // Procura pelas vizinhanças pelo maior valor do campo do rato.
          if ((field) && (field->getIdentifier() == "MouseField")) {
             fieldValue = field->valuePerceivedForAgent(this->getSelf());
             
             if (fieldValue > 0) {
                maxValue = fieldValue;
                string maxOwner = site->getSiteName();
                spSiteInfo_set::iterator site_i, s_end;
                site_i = localContext->beginAdjacentSites();
                s_end = localContext->endAdjacentSites();
                
                for (; site_i != s_end; ++site_i) {
                   field_i = (*site_i)->beginFields();
                   f_end = (*site_i)->endFields();
                   
                   for (; field_i != f_end; ++field_i) {
                   
                      if ((*field_i)->getIdentifier() == "MouseField") {
                         localValue = (*field_i)->valuePerceivedForAgent(
                            this->getSelf());
                         
                         if (localValue >= maxValue) {
                            maxOwner = (*site_i)->getSiteName();
                            maxValue = (*field_i)->getValue();
                         }
                      }
                   }
                }
                
                // Se o dono do valor max. não é o site que este agente
                // ocupa então pode caminhar.
                if (site->getSiteName() != maxOwner) {
                   this->setPossibleAction(
                      Transport::createTransport(maxOwner));
                   return;
                }
             }
          }
          
          // Em quaquer outra situação fica parado e emite campo.
          this->setPossibleAction(
             Emit::createEmit(spField(new CatField(20))));
       }
    }
    

    O método do rato é parecido e não será mostrado.

    A primeira fase (definição das classes) encerra-se aqui. A segunda fase começa com a construção das camadas. Para este exemplo, será construído apenas uma camada de 10x10 sítios. Cada sítio deve ter um nome, neste exemplo os nomes serão as coordenadas deles, por exemplo, o sítio "1x1" é um canto do mapa e o "10x10" está no canto oposto ao sítio anterior, assim como os sítios "1x10" e "10x1" também estão em cantos opostos.

    Antes de mostrar o código é preciso saber que existe somente uma instância da classe MMASS. Outro aspecto importante é a criação dos objetos. Em geral, um objeto A é criado pelo objeto B que contém A, por exemplo, um Space possui um conjunto de sítios, então um Site é criado pelo objeto Space e este Site pertencerá ao espaço que o criou. Veja o código da criação da camada abaixo.

    
    const unsigned int NUMROWS = 10;
    const unsigned int NUMCOLS = 10;
    
    void createSpace() {
       spMMASS mmass = CMMASS::getSingleton();
       spMASS mass = mmass->createMASS("ground");
       spSpace space = mass->createSpace();
       
       try {
       
          for (int i = 1; i <= NUMROWS; i++) {
       
             for (int j = 1; j <= NUMCOLS; j++) {
                char name[6];
                sprintf( name, "%dx%d", i, j);
                spSite site = space->createSite(string(name));
    
                if (j != 1) {
                   char left_name[6];
                   sprintf(left_name, "%dx%d", i, (j-1));
                   spSite left_site = space->getSite(string(left_name));
                   site->addAdjacentSite(left_site);
                }
    
                if (i != 1) {
                   char top_name[6];
                   sprintf(top_name, "%dx%d", (i-1), j);
                   spSite top_site = space->getSite(string(top_name));
                   site->addAdjacentSite(top_site);
                }
             }
          }
          space->finish();
       } catch (Space::logic_error &e) {
          fprintf(stderr, "%s", e.what().c_str());
          exit(-1);
       }
    }  
    

    Agora que a camada está construída, os agentes podem ser posicionados. Lembrando que só foi possível finalizar o espaço porque os sítios formaram um grafo conexo, caso contrário o método finish soltaria uma exceção. Agora os agentes serão colocados a uma distância de 3 sítios um do outro para conseguirem detectar suas presenças.

    
    void putAgents() {
       CatAgent::CatAgentFactory catFactory;
       MouseAgent::MouseAgentFactory mouseFactory;
       spMMASS mmass = CMMASS::getSingleton();
       spMASS mass;
       spAgent agent;
       spSite site;
       
       try {
          mass = mmass->getMASS("ground"); 
       
          agent = mass->createAgent(catFactory, "Cat");
          site = mass->getSpace()->getSite("1x1");
          agent->setSite(site);
       
          agent = mass->createAgent(mouseFactory, "Mouse");
          site = mass->getSpace()->getSite("2x3");
          agent->setSite(site);
          
          mass->finish();
       } catch (MASS::logic_error &e1) {
          fprintf(stderr, "%s", e1.what().c_str());
          exit(-1);
       } catch (MASS::invalid_argument &e2) {
          fprintf(stderr, "%s", e2.what().c_str());
          exit(-1);
       }
    }
    

    O MASS pôde ser finalizado porque seu espaço está finalizado e ele possui no mínimo um agente e todos eles estão posicionados em algum sítio.

    Resta agora finalizar e iniciar o MMASS. Veja o código abaixo.

    
    void runMMASS() {
       spMMASS mmass = CMMASS::getSingleton();
    
       try {
          mmass->finish();
          mmass->initiate();
       } catch (MMASS::logic_exception &e) {
          fprintf(stderr, "%s", e2.what().c_str());
          exit(-1);
       }
    }
    

    O MMASS foi finalizado com sucesso porque todas as camadas estão finalizadas (minha implementação não aproveita a característica de multicamadas do MMASS, então a classe Interface não foi implementada e por isso o MMASS não checa se todas as camadas estão interligadas como diz a definição 6). Caso contrário método finish soltaria uma exceção.

    O método initiate cria e inicia todas as threads (dos agentes e do ambiente). A classe MMASS dá acesso à thread do ambiente (a estrutura pthread_t da biblioteca Pthread), e assim é possível esperar pelo encerramento dela usando o método pthread_join. Para encerrar o sistema basta ligar a flag finish_thread da classe MMASS. O usuário não precisa se preocupar com as threads dos agentes, pois a própria thread do ambiente encerra e espera pela finalização dos agentes.

    Aqui encerra a seção sobre o MMASS. Com o framework pronto o próximo passo é integrar este sistema com a parte gráfica, este é o assunto da próxima seção.


    Integração com a parte gráfica [índice]

    Para a parte gráfica deste projeto utilizei a Ogre, acrônimo de Object-Oriented Graphics Rendering Engine. Este renderizador é relativamente fácil de ser usado, é bem documentado, e possui uma comunidade grande e ativa de usuários, e essas características me levaram a escolhê-lo para a aplicação neste projeto. Ao contrário das outras bibliotecas usadas, não farei uma introdução sobre a Ogre aqui, pois não será necessário para entender esta seção.

    Até o momento em que esta monografia foi escrita. O projeto se encontrava nesta fase. A integração da parte gráfica com o MMASS expôs algumas falhas de projeto do framework, que do ponto de vista do trabalho é interessante mostrá-los.

    Falhas de projeto [índice]

    Na primeira tentativa de integrar a Ogre e o MMASS ficou claro que implementar o MMASS como um programa multithread não foi uma boa escolha. Fazendo um teste com dois agentes, totalizando quatro threads (dois do agente, um do ambiente e um do renderizador), o resultado foi que um turno demorava alguns segundos para passar (ou seja, os agentes demoravam segundos para andar de um sítio para o outro, por exemplo), e o desempenho do renderizador era afetado drasticamente (ou seja, a animação era "quebrada", ou era gerada a menos de 25 quadros por segundo).

    Resolver este problema foi fácil. Com a versão multithread implementada, basta modificá-la de forma a seguir a sequência esperada que as threads tomassem para que o sistema avançasse de turno. Não vou mostrar o código modificado aqui, o mais importante é entender como o mecanismo funciona e tanto a versão multithread quanto a versão sequencial seguem este mesmo mecanismo.

    O resultado da modificação foi o esperado. Com o mesmo exemplo de dois agentes, agora totalizando duas threads (um do renderizador e outro para todo o MMASS), o resultado foi uma animação suave e os turnos demoram bem menos para passarem. Porém, ainda há um problema relacionada com threads que será comentada mais adiante.

    Obs.: Até o momento que esta monografia foi escrita, o projeto se encontrava neste ponto. Os problemas comentados abaixo ainda não foram corrigidos.

    Outro problema é o consumo de memória. O consumo alto já era esperado e o leitor pode ter percebido isso na seção anterior. Na classe Site, foi dito que um sítio guarda a distância dele para todos os outros sítios, ou seja, se um sistema é formado por n sítios, então cada um guarda n - 1 distâncias o que nos dá um consumo de memória de ordem quadrática. Em um exemplo com um mapa de 35x35 sítios (1225 sítios), que não é um ambiente grande para um jogo, foi consumido toda minha memória (de 1024mb).

    Resolver este problema também não é difícil. Basta que as distâncias sejam sempre calculadas na momento que for necessário, dessa forma não é necessário guardá-las. Porém, em um sistema onde as distâncias precisam ser consultadas constantemente isso pode não ser viável e uma solução intermediária deve ser adotada, como por exemplo, guardar as distâncias calculadas temporariamente. Temos aqui o velho trade-off da eficiência vs. memória.

    Foi dito agora a pouco que o sistema é formado por duas threads. A thread principal é o renderizador, e este cria uma thread separada para executar o MMASS concorrentemente, o principal motivo desta decisão é que o renderizador não pode parar para esperar o MMASS atualizar os agentes. Mas sincronizar estas duas threads não é simples. O renderizador possui um looping como o pseudo-código mostrado abaixo.

    
    while (continua) {
       consulta o estado dos agentes;
       
       desenha os agentes;
       
       if (hora de atualizar os agentes)
          pede para o MMASS atualizar os agentes
    }
    

    Uma iteração corresponde a um quadro da animação, mas um quadro não representa um turno do MMASS, por isso este não pode ser atualizado sempre. O problema é saber quando que os agentes devem ser atualizados, e mesmo assim a thread do MMASS pode demorar a ser executada depois de ser sinalizada para atualizar. O resultado dessa dessincronização é uma ligeira pausa entre os turnos dos agentes. Outra alternativa é fazer a integração integração de forma que o programa final não seja multithread. Isso certamente causará uma queda na performance do renderizador devido às pausas de atualização, mas mesmo assim é uma alternativa a ser considerada.

    No link abaixo há um vídeo do resultado atual do projeto. Uma bola azul é um agente que emite um campo que espanta os outros agentes. Repare nas pausas no movimento dos agentes, fruto da falha de sincronização entre as threads.

    Vídeo

    O código fonte do trabalho até o presente momento está no link abaixo.

    Código-fonte

    Conclusões [índice]

    As definições formais são uma abstração do framework no sentido de que a partir dessas definições podemos concretizar nossa própria implementação em diferentes domínios, por exemplo, o MMASS pode ser aplicado em jogos e portanto a implementação será um programa de computador, ou o sistema pode ser aplicado para distribuir informações para computadores móveis e portanto ele não será somente um software. Além disso, as definições e os pseudo-códigos consegue descrever o MMASS com um bom nível de detalhes. Portanto, para um desenvolvedor seguindo o paradigma de programação orientada a objeto, o trabalho do Vizzari poupa bastante esforço para projetar uma implementação própria. Neste sentido o trabalho do Vizzari é ótimo, uma boa parte do trabalho discute diversas possibilidades de implementação.

    Porém, na minha opinião, faltou detalhar mais sobre a sincronização entre as threads (dos agentes e do ambiente) no caso síncrono. Por este motivo e pelo fato do meu trabalho ser uma implementação concreta, neste trabalho procurei mostrar com um pouco mais de detalhe como faço a sincronização.

    A utilização do MMASS em jogos é uma boa idéia. Neles são comum um grupo de agentes ter a necessidade de agir em conjunto para alcançar um objetivo, por exemplo, policiais agem como um time para invadir uma base de terroristas. Três fatores torna o framework adequado para este tipo de aplicação: a sua flexibilidade, a facilidade de definir o sistema multiagente, para isso basta definir os agentes e suas ações, e a separação do agente e do gerenciamento das ações.

    Na prática, a viabilidade de sua implementação depende da disponibilidade de alguns recursos como a quantidade de memória e do tempo de processamento (pensando no caso de um software multithread, o renderizador de um jogo deve ter prioridade sobre outras threads, então o tempo que sobra para o MMASS fazer suas operações é suficiente para atualizar os agentes e ter o resultado desejado?).

    Apesar do estudo de MASs ser uma área de IA, projetar um MAS é uma tarefa multidisciplinar. Neste sentido acho que este foi um ótimo trabalho de formatura, pois pude encontrar vários conceitos estudados durante todo o curso tanto na parte do estudo sobre o MMASS quanto na parte da implementação.


    Desafios e frustrações [índice]

    C++

    Como aluno do IME, eu me acostumei a programar em Java. No começo das atividades de programação deste trabalho foi difícil a "transição" de Java para C++. A principal dificuldade é lembrar que nem todas as variáveis são referências para um objeto, e por causa disso é preciso sempre ter em mente conceitos como bitcopy vs. inicialização, passagem de parâmetro por valor vs. passagem de parâmetro por referência, copy-constructor, etc [2].

    Além disso, como C++ não tem coletor de lixo, o fato de ter que liberar memória manualmente afeta um pouco no projeto de uma classe. Se um objeto A guarda um apontador para outro objeto B, devemos decidir se o dono da referência (apontador) é o usuário da classe A ou a própria classe A, ou seja, decidir quem é responsável por liberar a memória de B depois que este não é mais necessário. (Na verdade, usei apontadores inteligentes da biblioteca Boost que resolveu o problema de liberação manual da memória, mas não resolveu o problema de decidir quem é o dono da referência.)

    Por outro lado, a experiência com C++ me fez refletir um pouco sobre os motivos do IME adotar Java (e o curso de POO adotar o Smalltalk). Acredito que para fins didáticos Java é mais adequado do que C++.

    Visual do exemplo (modelagem)

    Este assunto não está relacionado com o curso de computação, mas foi algo que me frustrou. Eu realmente queria fazer um exemplo mais bonito, porém a falta de conhecimento e de prática com algum programa de modelagem 3D me impediram de conseguir um exemplo vistoso.

    Programação concorrente

    Fazer a sincronização entre as threads foi difícil. Não é possível criar testes unitários para ter segurança de que o comportamento das threads será como o esperado. Também é difícil de encontrar o erro quando algo errado acontece. Neste trabalho usei muitas impressões (printfs em arquivos no formato html para facilitar a vizualização) para acompanhar o comportamento das threads.

    No caso síncrono (que é o caso deste trabalho) o trabalho do Vizzari não dá muitos detalhes sobre a sincronização entre as threads, o que também dificultou nesta parte da implementação.

    Matérias relevantes [índice]

    Foram muitas matérias que me ajudaram diretamente na realização deste trabalho. A começar por Inteligência Artificial, o MMASS é relacionado a esta área e os conceitos desta matéria foram importante para entender o artigo [1]. As matérias Análise de algoritmos e Linguagens formais e autômatos ajudaram a entender alguns exemplos do artigo. Programação em redes de computadores também ajudou a entender algumas colocações no artigo sobre a comunicação entre os agentes.

    É difícil não citar Introdução à Computação e Princípios de Desenvolvimento de Algoritmos em um trabalho que envolveu programação.

    Algoritmos em grafos e Programação concorrente foram importantes tanto para compreender o MMASS como para implementá-lo. Este sistema faz uso intensivo de algoritmos como busca em largura e busca em profundidade, e por se tratar de um programa multithread foi necessário garantir exclusão mútua em seções críticas e sincronização entre as threads.

    Laboratório de programação II por ter ensinado conceitos básicos de POO, Programação orientada a objeto por ter ensinado conceitos avançados de POO, padrões e refatoração e o curso de Engenharia de software foram disciplinas que falaram sobre assuntos relacionados a projeto de software e me ajudaram a implementar o MMASS. Laboratório de programação extrema me ajudou com algumas práticas de programação como o teste a priori, integração continua, soluções simples, etc.

    Para aprender C++, a matéria de Conceitos de linguagens de programação foi importante para entender o livro [2], inclusive entender que muitas decisões do projeto da linguagem C++ priorizaram o desempenho.

    Também usei um pouco de Algebra Linear para manipular os agentes em um abiente 3D.

    A quantidade de matéria envolvidas diretamente com a realização deste trabalho mostra que os conceitos aprendidos durante o curso foram fundamentais. É gratificante pensar que consegui entender um trabalho de doutorado (apesar de eu ter a impressão que o trabalho não é difícil de entender) e todos os outros assuntos estudados, e também conseguir colocá-los em prática.

    Considero positivo a experiência adquirida por ter feito um sistema relativamente grande, algo que não foi muito comum durante o curso de Computação. Porém esta experiência poderia ter sido melhor se não fosse realizado sozinho, algo que eu deveria ter levado em consideração antes.

    Para me aprofundar no que foi envolvido neste projeto, acredito que poderia seguir dois caminhos. Um seria voltado para a parte teórica e seguiria estudando IA e MASs. O outro caminho seria voltado para a prática e estudaria mais sobre matérias relacionadas a construção e projeto de softwares, como O.O., Engenharia de Software, etc, e tentaria melhorar minha implementação para disponibilizá-lo à comunidade da Ogre.

    Agradecimentos [índice]

    Gostaria de agradecer ao professor Flávio, sem sua sugetão eu não saberia o que fazer como trabalho de formatura, certamente elaborar um trabalho foi um desafio. Ele também ajudou a me organizar para a realização desta tarefa.

    Além disso, participar da iniciação científica foi uma experiência muito boa. Ter escrito um pequeno artigo (não relacionado a este trabalho) e apresentado em um Simpósio foi algo novo para mim e, de certa forma, emocionante (fiquei nervosíssimo para apresentar). E graças a esta iniciação eu despertei novamente o intresse em estudar e, principalmente, ir atrás de um dos assuntos que me motivou a fazer o curso de Ciência da Computação, os jogos de computadores.

    Agradeço também aos meus colegas de faculdade por terem me suportado por 4/5 anos e terem ajudado a tornar mais agradável todos esses anos na faculdade.

    Também queria agradecer aos meus amigos por serem ótimos companheiros, e por terem ajudado de alguma forma a eu chegar até aqui.

    Em especial, gostaria de agradecer aos meus pais por terem me dado toda as condições (afetivo e fincanceiro) para eu me formar. Certamente, o meu diploma também é deles por terem se esforçado em dar uma educação de boa qualidade.

    Bibliografia [índice]

    • [1] Vizzari, G. (2005) Dynamic Interaction Spaces and Situated Multi-Agent Systems: from a Multilayered Model to a Distributed Architecture
    • [2] Eckel, B. (2000) Thinking in C++ vol. 1
    • [3] Eckel, B. (2000) Thinking in C++ vol. 2
    • [4] Sedgewick, R. Algorithms in C, part 5: Graph algorithms
    • [5] Andrews, G. Foundations of Multithreaded, Parallel, and Distributed Programming
    • [6] Laird, J. (2001) Human - level AI's Killer Application: Interactive Computer Games

    topo