Programação concorrente

1 view
Skip to first unread message

le-silva

unread,
Jul 3, 2009, 11:40:54 PM7/3/09
to Programming Talk
Senhores!!!

Eu tenho bastante interesse em programação concorrente, paralelismo,
por isso frequentemente estou testando alguma técnica, implementando
alguma coisa do genero. Tempo atrás acabei implementando um
microframework em Java para concorrência baseada em atores.

Bem, a questão que eu queria trazer aqui é sobre "Shared Memory x
Actor Based". Alguns defendem o primeiro, como é o caso de Clojure;
outros defendem o segundo, como é o caso de Scala.

E vocês? Qual a preferência, que tenha têem a jogar na fogueira?

Ronaldo Ferraz

unread,
Jul 4, 2009, 12:28:12 PM7/4/09
to programm...@googlegroups.com
Só um detalhe de terminologia, o correto seria dizer STM (Software
Transactional Memory) versus Actors, já que ambos são modos de
compartilhar Shared Memory sem usado de locks explícitos.

Eu estou longe de ser uma especialista, mas depois de brincar um pouco
com Clojure, Scala e Erlang--e temporariamente me decidir pelo último
para implementar alguns projetos pessoais e aprender algo fora do
usual do que eu já tinha estudo, eu confesso que gosto mais do modo
Scala e Erlang por não me forçar a explicitamente pensar sobre
concorrência.

Em Erlang, especialmente, concorrência está quase que ausente do
vocabulário da linguagem. Não há necessidade, em quase nenhum ponto,
de pensar em algum aspecto essencial de controle de memória nem quando
se lidar com o acesso de recursos extremamente seqüenciais como
arquivos e sockets de comunicação.

Scala segue Erlang de perto mas oferece uma sintaxe mais próxima do
mundo OO por suas raízes na JVM. Como Erlang, estruturas de dados e
estado podem ser um pouco confusas se as convenções da linguagem não
forem seguidas de perto.

Aliás, segue o projeto que comecei a implementar usando Erlang usando
esses conceitos (ainda está bem no início e não subi tudo que tenho
aqui): http://github.com/rferraz/eleusis-server/tree/master

Nesse projeto, um único detalhe que eu tive que expor foi a troca de
controle do módulo que supervisiona um socket pelo motivo que
mensagens só são enviadas para um módulo por um socket de sistema.

Abraços,

R.

2009/7/4 le-silva <leand...@gmail.com>:

le-silva

unread,
Jul 5, 2009, 1:03:07 AM7/5/09
to Programming Talk
> Só um detalhe de terminologia, o correto seria dizer STM (Software
> Transactional Memory) versus Actors, já que ambos são modos de
> compartilhar Shared Memory sem usado de locks explícitos.

Opa! Valeu a observação, Ronaldo.

É verdade, Clojure e Scala, apesar de seguirem paradigmas de
concorrência diferentes, em última analise, são modos de concorrência
implementados sobre shared memory. Mas isso por conta da plataforma
hosting delas -- Java --, que é essencialmente shared memory.

Então duas coisas:

1- Fui muito genérico ao citar apenas "shared memory", sem ser mais
especifico quanto ao STM implementado por Clojure;

2- Por causa disso, meu exemplo de Scala foi por água abaixo. Porque o
modelo de atores de Scala, apesar de ser semanticamente message
passing, fundamentalmente, é shared memory com locks implicitos,
implementados pela biblioteca de atores da linguagem. (Aliás, você já
deu uma olhada nos fontes da biblioteca de atores de Scala? Eu fiz
isso há um tempo atrás. Threads Java puras, como esperado... hehehe)

Bem, neste caso, teria sido muito melhor se eu tivesse citado Erlang,
que em lugar de fazer message passing com memória compartilhada, usa
processo ultra-leves, como você sabe muito bem.


> Eu estou longe de ser uma especialista, mas depois de brincar um pouco
> com Clojure, Scala e Erlang--e temporariamente me decidir pelo último
> para implementar alguns projetos pessoais e aprender algo fora do
> usual do que eu já tinha estudo, eu confesso que gosto mais do modo
> Scala e Erlang por não me forçar a explicitamente pensar sobre
> concorrência.

SOMOS DOIS ENTÃO!!! =p

É, essa também é a minha preferência, e foi por isso que resolvi
implementar o tal micro-framework de atores para Java. Eu também não
queria ter que lidar explicitamente com locks e afins.

Na verdade, eu concei usando Jetlang, por indicação do nosso caro
Phillip Calçado. Mas acho que Jetlang, apesar de ser bem interessante,
bem aceita, etc, ainda deixa vazar muitos detalhes de concorrência
que, na minha opinião, criam muito ruido no código; e eu queria uma
coisa mais clean, mais próxima dos atores de Scala.

Eu acho o modelo de atores bem fluente, natural, desacoplado. IMHO.

Na época, eu cheguei a dar uma olhada em uma implementação bem
interessante em Groovy, que me inspirou bastante:

http://code.google.com/p/gparallelizer

Pela que a linguagem Java não tenha uma sintaxe tão flexivel. =( Mas
deu pra fazer uma coisa interessante. Pena eu não ter um código aqui,
agora, pra mostrar, porque implementei isso pra empresa que trabalho.
Na segunda-feira prometo que posto um exemplinho -- claro, se você
tiver interesse, não sei se você tem muito interesse em Java, sei lá.

Quero ver se arranja um tempo para re-implementar esse micro-framework
aqui em casa, como código aberto, publicado no meu github.


> Em Erlang, especialmente, concorrência está quase que ausente do
> vocabulário da linguagem. Não há necessidade, em quase nenhum ponto,
> de pensar em algum aspecto essencial de controle de memória nem quando
> se lidar com o acesso de recursos extremamente seqüenciais como
> arquivos e sockets de comunicação.

Já andei brincando um pouco com os atores de Erlang também e gostei
bastante. Pena que estou meio sem tempo pra continuar meu aprendizado
de Erlang agora. =(


> Scala segue Erlang de perto mas oferece uma sintaxe mais próxima do
> mundo OO por suas raízes na JVM. Como Erlang, estruturas de dados e
> estado podem ser um pouco confusas se as convenções da linguagem não
> forem seguidas de perto.

Principalmente se não for seguida a boa prática de deixar o código
concorrente livre de efeito colateral. Afinal, concorrencia e efeito
colateral na mesma frase não combinam nem a pau.


> Aliás, segue o projeto que comecei a implementar usando Erlang usando
> esses conceitos (ainda está bem no início e não subi tudo que tenho
> aqui):http://github.com/rferraz/eleusis-server/tree/master

Vou dar uma olha no seu projeto, me parece interessante.


> Nesse projeto, um único detalhe que eu tive que expor foi a troca de
> controle do módulo que supervisiona um socket pelo motivo que
> mensagens só são enviadas para um módulo por um socket de sistema.
>
> Abraços,
>
> R.

Estou gostando bastante dessa lista. Alto padrão. Parabéns pela
iniciativa!

Abraço,

Leandro


> 2009/7/4 le-silva <leandrod...@gmail.com>:

le-silva

unread,
Jul 20, 2009, 6:13:39 PM7/20/09
to Programming Talk
Para quem tem interesse em concorrência e afins, esse blog tem uns
artigos bacanas:

http://bartoszmilewski.wordpress.com

Ronaldo Ferraz

unread,
Jul 20, 2009, 6:22:46 PM7/20/09
to programm...@googlegroups.com
Opa!

2009/7/5 le-silva <leand...@gmail.com>:


> 2- Por causa disso, meu exemplo de Scala foi por água abaixo. Porque o
> modelo de atores de Scala, apesar de ser semanticamente message
> passing, fundamentalmente, é shared memory com locks implicitos,
> implementados pela biblioteca de atores da linguagem. (Aliás, você já
> deu uma olhada nos fontes da biblioteca de atores de Scala? Eu fiz
> isso há um tempo atrás. Threads Java puras, como esperado... hehehe)

Eu não cheguei a olhar, mas um colega de trabalho meu está justamente
re-implementando um programa aqui em Scala e está gostando muito do
modelo de concorrência. Segundo ele, conseguiram esconder bastante
coisa de modo que, barrando alguns poucos casos onde a abstração vaza,
dá para fazer muita coisa legal sem perceber que são Java threads.

Aliás, acho muito bacana, de um ponto de vista de linguagem pura, que
Scala consiga implementar toda a abstração recorrendo somente à sua
própria sintaxe e semântica, sem necessidade de suporte nativo no
compilador. Eu estou estudando um pouco a linguagem, fazendo alguns
scripts aqui e ali, e gostei de várias das decisões que eles tomaram
para isso.

> Pela que a linguagem Java não tenha uma sintaxe tão flexivel. =(  Mas
> deu pra fazer uma coisa interessante. Pena eu não ter um código aqui,
> agora, pra mostrar, porque implementei isso pra empresa que trabalho.
> Na segunda-feira prometo que posto um exemplinho -- claro, se você
> tiver interesse, não sei se você tem muito interesse em Java, sei lá.

Manda bala. Acabei demorando para responder, mas sempre estou interessado. ;)


> Principalmente se não for seguida a boa prática de deixar o código
> concorrente livre de efeito colateral. Afinal, concorrencia e efeito
> colateral na mesma frase não combinam nem a pau.

Sim. Gostei da diferença entre val e var no Scala justamente por dar
essa flexibilidade. Só não gostei das palavras-chave serem tão
parecidas. Por causa de uma consoante, nego pode perder horas
procurando um bug. :)

> Vou dar uma olha no seu projeto, me parece interessante.

Preciso só voltar a codar. :)

Abraços,

R.

le-silva

unread,
Jul 20, 2009, 7:06:48 PM7/20/09
to Programming Talk
Opa Ronaldo!

Então, cara, esse é um exemplo bem simples que ilustra a semantica de
uso de um dos tipos de atores que implementei.

Digo um dos tipos, porque acabei implementando duas tipos:

1- Baseados em eventos, implementados como classes (com seus
respectivos arquivos .java), implementando alguns métodos para
tratamento dos eventos que serão notificados: onStart, react, onStop e
onException.

Há ainda dois subtipos: Timeout e Timeless. O tipo Timeout responde
também à notificação de onExpire.

(Mas é opcional o tratamento desses eventos.)

2- Bloqueante, implementados de maneira como no exemplo abaixo.

Eu o chamei de bloqueante, por causa da natureza dele de ficar
bloqueado no método receive, aguardando por uma mensagem.

Bem, essa é a idéia básica deles. A implementação que eu fiz, como
disse antes, fiz aqui para a empresa, por isso não posso publicar o
código todo -- bem, você sabe como é que é. Mas como eu disse também,
eu quero muito tirar um tempo em casa para reimplementar e melhorar
essa idéia. Só preciso arranjar tempo. ;-)

Abraço!

=======================================

public class AppBlockerBased1 {

public static void main(String[] args) {
System.out.println(">>> Iniciando o EchoServer");

Actor echoServer = new BlockerActor() {
protected void act() {
System.out.println("EchoServer está de pé!");

Message message;

while (true) {
message = receive();

if (message.content() instanceof String) {
System.out.println("Mensagem [String]: " +
message.content());
} else if (message.content() instanceof Map) {
System.out.println("Mensagem [Map]: " +
message.content());
} else if (message.content() instanceof Stop) {
System.out.println("Mensagem [Stop]: " +
message.content() + " (fui!)");
stop(); // finaliza o ator
break; // sai do while
} else {
System.out.println("Mesangem de tipo
desconhecido: " + message.content());
}
}
}
};
echoServer.start();

System.out.println(">>> Enviando 'olá!' ao EchoServer");
echoServer.send("olá!");

System.out.println(">>> Enviando '12' ao EchoServer");
echoServer.send(12);

System.out.println(">>> Enviando um 'Map' ao EchoServer");
Map<String, Boolean> map = new HashMap<String, Boolean>();
map.put("server_up", true);
map.put("run_until_stop_message", true);
echoServer.send(map);

System.out.println(">>> Enviando um 'Stop' ao EchoServer");
echoServer.send(Stop.NOW);

System.out.println(">>> Fim da aplicação");
}

}

=======================================


On 20 jul, 19:22, Ronaldo Ferraz <ronaldofer...@gmail.com> wrote:
> Opa!
>
> 2009/7/5 le-silva <leandrod...@gmail.com>:

Rodrigo Kumpera

unread,
Jul 20, 2009, 7:05:46 PM7/20/09
to programm...@googlegroups.com


2009/7/20 Ronaldo Ferraz <ronald...@gmail.com>


Opa!

2009/7/5 le-silva <leand...@gmail.com>:
> 2- Por causa disso, meu exemplo de Scala foi por água abaixo. Porque o
> modelo de atores de Scala, apesar de ser semanticamente message
> passing, fundamentalmente, é shared memory com locks implicitos,
> implementados pela biblioteca de atores da linguagem. (Aliás, você já
> deu uma olhada nos fontes da biblioteca de atores de Scala? Eu fiz
> isso há um tempo atrás. Threads Java puras, como esperado... hehehe)

Eu não cheguei a olhar, mas um colega de trabalho meu está justamente
re-implementando um programa aqui em Scala e está gostando muito do
modelo de concorrência. Segundo ele, conseguiram esconder bastante
coisa de modo que, barrando alguns poucos casos onde a abstração vaza,
dá para fazer muita coisa legal sem perceber que são Java threads.

Aliás, acho muito bacana, de um ponto de vista de linguagem pura, que
Scala consiga implementar toda a abstração recorrendo somente à sua
própria sintaxe e semântica, sem necessidade de suporte nativo no
compilador. Eu estou estudando um pouco a linguagem, fazendo alguns
scripts aqui e ali, e gostei de várias das decisões que eles tomaram
para isso.

Eu tentei ano passado usar Actors do Scala mas achei um pouco limitado
aquilo que pode ser feito. Principalmente quando se trata de código já existente.

No caso tentei adaptar um parser p/ um protocolo para funcionar com actors
mas não deu muito certo já que para o treco todo funcionar você tem que bloquear
to top-level receive.

Em 2006 eu implementei em Java algo semelhante aos actors do Scala porém
usando CPS, oque permitia que eu desse yield arbitŕario. A performance não era
meteórica devido a forma como foi feito a transformação p/ CPS (usei uma biblioteca
de continuations da apache), mas permitia talks usando pucos kb de memória.

Outra coisa com que brinquei recentemente foi o suporte a delimited continuations
que tem no mono. É meio complicado de entender como funciona no começo, mas
tem a vantagem de ter suporte da VM, então é mais rápido que via CPS no java.

Apesar disso, quero voltar a usar Scala quando integrarem no compilador final
isso aqui: http://lamp.epfl.ch/~phaller/uniquerefs que promete ser bem interessante.

Ronaldo Ferraz

unread,
Jul 20, 2009, 8:27:28 PM7/20/09
to programm...@googlegroups.com
Interessante. Pena que não dá para mostrar mais detalhes, fiquei
curioso para saber um pouco da implementação interna de algumas
coisas.

Se posso oferecer uma sugestão, pelo modo como o código foi
implementado talvez faça sentido o loop while por algum tipo de
mailbox mais explícita. Não porque não funciona, mas talvez deixe o
código semanticamente mais interessante.

Um ponto chato do Java puro é que é bem complicado implementar DSLs
decentes na linguagem. Imagino que o código acima seja bem acessível a
patterns via DSLs nesse sentido.

R.

2009/7/20 le-silva <leand...@gmail.com>:

Ronaldo Ferraz

unread,
Jul 20, 2009, 8:42:10 PM7/20/09
to programm...@googlegroups.com
2009/7/20 Rodrigo Kumpera <kum...@gmail.com>:

> Eu tentei ano passado usar Actors do Scala mas achei um pouco limitado
> aquilo que pode ser feito. Principalmente quando se trata de código já
> existente.

Eu confesso que não cheguei a entrar em todos detalhes possíveis mas
no que o meu colega precisou na implementação o resultado foi
satisfatório. De fato, o código que foi criado não usa nada de
especial e não tem nenhum requerimento muito complicada. E, por razões
várias, Scala faz mais sentido para nós agora do que Erlang e foi uma
substituição até bem tranqüila. Imagino que para necessidade bem mais
complexas possa haver alguma falha na implementação, mas não cheguei
ainda nesse ponto.

> No caso tentei adaptar um parser p/ um protocolo para funcionar com actors
> mas não deu muito certo já que para o treco todo funcionar você tem que
> bloquear
> to top-level receive.

Pelo que eu andei lendo na literatura, embora não seja considerado de
boa forma bloquear no top-level, há maneiras de contornar isso usando
helper actors. Não sei, é claro, se era aplicável no seu caso.

Abraços,

R.

le-silva

unread,
Jul 20, 2009, 11:11:25 PM7/20/09
to Programming Talk
> Interessante. Pena que não dá para mostrar mais detalhes, fiquei
> curioso para saber um pouco da implementação interna de algumas
> coisas.

Pois é, chato isso. Minha idéia é re-implementar e botar no meu
GitHub, pra trocar idéias e evoluir a coisa.

Mas, assim, pra dar uma idéia de como implementei:

1- Cada ator tem uma Mailbox própria que tem, entre outras coisas, a
capacidade de ficar bloqueada esperando por uma mensagem, se for o
caso do tipo de ator;

2- Cada ator tem uma thread (que eu chamo carinhosamente de asyncAct)
onde acontece sua reação a mensagens que chegam;

3- Atores Timeout e Timeless são baseados em eventos, então sua
asyncAct só acorda quando chega uma mensagem ou quando vence o tempo
estabelecido entre mensagens;

4- TimeoutActor foi criado pra tentar evitar memory leak, quando por
algum erro bizarro acontecer. Assim, caso algum erro aconteceça o ator
não recebe uma mensagem STOP devidamente, vencendo o tempo de timeout,
ele encerra suas atividades e bye!

5- TimelessActor por outro lado, sinto muito. Se ele não receber uma
mensagem de STOP, I'm so sorry!

6- Atores Blocked, como mostrei no exemplo, ficam bloqueados esperando
uma mensagem chegar.

Bem, sobre os métodos callback dos eventos, acho que foi uma boa,
porque há situações que você vai querer fazer algum setup antes de
reagir alguma mensagem; vai querer fazer alguma limpeza antes de
finalizar as atividades do ator; e, com certeza, se ocorrer algum
erro, você vai querer tomar alguma atitude como, por exemplo, guardar
a mensagem em algum lugar para ser processada posteriormente -- ou
fazer algum esquema na linha de Erlang: botar algum ator pra tratar a
mensagem que, por algum motivo, você falhou ao tratar.

> Se posso oferecer uma sugestão, pelo modo como o código foi
> implementado talvez faça sentido o loop while por algum tipo de
> mailbox mais explícita. Não porque não funciona, mas talvez deixe o
> código semanticamente mais interessante.

Eu tentei chegar ao máximo próximo da sintaxe e semântica dos atores
de Scala -- o gparallelizer também tenta fazer isso [com bastante
sucesso, aliás]. Mas gostei da sua idéia do while na Mailbox. Vou
tentar implementar isso deplois pra ver como fica. Como te disse, tem
muito que evoluir, e acredito que isso só aconteça com bate-papos,
compartilhamento. ;-)

> Um ponto chato do Java puro é que é bem complicado implementar DSLs
> decentes na linguagem. Imagino que o código acima seja bem acessível a
> patterns via DSLs nesse sentido.

Pois é, uma das coisas que mais sinto falta e Java é de pattern
matching decente; e não esse encadeamento de if tosco. Se ao menos
desse pra usar um intanceof no switch-case, já seria uma mão na roda.

Não sei se você chegou a dar uma olhada no gparallelizer que citei em
um dos posts. Bem, em Groovy o matching da mensagem ficou bem melhor.

Ah! Uma coisa que quero implementar é um tipo de monitor de atores,
saca? Queria ter um monitor pra saber quantos atores estão em
atividade e de que tipo; queria pode, se for o caso, através desse
monitor, mandar pro lixo atores perdidos (memory leak, lost threads).
Alguma coisa assim.

[]s
Leandro

> 2009/7/20 le-silva <leandrod...@gmail.com>:

le-silva

unread,
Jul 20, 2009, 11:38:22 PM7/20/09
to Programming Talk
Duas coisas que você comenta em seu post:

http://www.kumpera.net/blog/index.php/2007/03/01/java-e-concorrencia-via-actor

"O maior problema é que Java não é a plataforma ideal para se
implementar Actors da maneira ideal. Você é obrigado a assumir vários
trade-off para chegar em uma solução escalavel. Primeiro que utilizar
uma thread por actor não vinga, dificilmente um sistema suportaria
mais de 3mil deles. Um programa erlang pode ter milhares de micro-
processos sem nenhum problema."

Uma coisa que tive que fazer em minha implementação foi não manter as
threads vivas o tempo todo. Fiz alguns testes e não demorava muito pra
JVM explodir por falta de threads... hehehe... Então, mantenho-nas
vivas o mínimo possível.

"Finalmente tem o problema que apenar de usar um modelo de troca de
mensagens que não precisa de memória compartilhada, não podemos nos
dar o luxo de ter actors que simplesmente abortam na presença de
erro."

Esse foi o motivo de eu prover um onException, pra que o ator possa
fazer algo se alguma falha ocorrer.

---

Por que não usamos Scala de uma vez, hein? Já fizeram todo esse
trabalho por nós...hehehe

(Bem, no meu caso não dá mesmo, tem que ser Java. OmG!)

[]
Leandro

On 20 jul, 20:05, Rodrigo Kumpera <kump...@gmail.com> wrote:
> 2009/7/20 Ronaldo Ferraz <ronaldofer...@gmail.com>
>
>
>
>
>
> > Opa!
>
> > 2009/7/5 le-silva <leandrod...@gmail.com>:

Rodrigo Kumpera

unread,
Jul 21, 2009, 12:19:37 AM7/21/09
to programm...@googlegroups.com


2009/7/21 le-silva <leand...@gmail.com>


Duas coisas que você comenta em seu post:

http://www.kumpera.net/blog/index.php/2007/03/01/java-e-concorrencia-via-actor

"O maior problema é que Java não é a plataforma ideal para se
implementar Actors da maneira ideal. Você é obrigado a assumir vários
trade-off para chegar em uma solução escalavel. Primeiro que utilizar
uma thread por actor não vinga, dificilmente um sistema suportaria
mais de 3mil deles. Um programa erlang pode ter milhares de micro-
processos sem nenhum problema."

Uma coisa que tive que fazer em minha implementação foi não manter as
threads vivas o tempo todo. Fiz alguns testes e não demorava muito pra
JVM explodir por falta de threads... hehehe... Então, mantenho-nas
vivas o mínimo possível.


É uma alternativa, outra é usar um pool de threads, que basicamente elimina
o custo de criação.
 

"Finalmente tem o problema que apenar de usar um modelo de troca de
mensagens que não precisa de memória compartilhada, não podemos nos
dar o luxo de ter actors que simplesmente abortam na presença de
erro."

Esse foi o motivo de eu prover um onException, pra que o ator possa
fazer algo se alguma falha ocorrer.


Isso é mais uma observação geral sobre sistemas com memória compartilhada.
A maioria esmagadora deles corrompe o estado na presença de exceptions assíncronas.
Eu nunca encontrei um programa que não fizesse besteira se você chamar Thread::Abort()[1]
ou, menos brusco, que fizesse error recovery certo p/ qualquer opção.

Nesse ponto erlang é mais fácil e seguro, tratamento de erro maioria das vezes é crashar, oque
é bem simples e tem a garantia de não corromper o estado geral.

[1]O que é uma afirmação injusta, dado que nenhum consegue já que nem BCL da Sun consegue.


le-silva

unread,
Jul 21, 2009, 8:34:39 AM7/21/09
to Programming Talk
> É uma alternativa, outra é usar um pool de threads, que basicamente elimina
> o custo de criação.

Eu tentei trabalhar com o cached thread pool, mas sempre estourava o
número máximo de threads suportadas. Desta outra maneira, apesar de
ser mais rudimentar, suporta melhor a bronca sem crashear.

> Isso é mais uma observação geral sobre sistemas com memória compartilhada.
> A maioria esmagadora deles corrompe o estado na presença de exceptions
> assíncronas.
> Eu nunca encontrei um programa que não fizesse besteira se você chamar
> Thread::Abort()[1]
> ou, menos brusco, que fizesse error recovery certo p/ qualquer opção.

Acho que a técnica aqui pra evitar (ou ao menos suavizar) erros desta
natureza é arquitetar direitinho o workflow das mensagens. Se aninhar
minhas trocas de mensagens entre atores, fica ultra dificil de fazer
recovery e mesmo debugar erros.

le-silva

unread,
Jul 21, 2009, 8:46:10 AM7/21/09
to Programming Talk
Só um detalhe: Pode ser que eu me esqueci -- ou mesmo não soube --
"apertar algum parafuso" para o cached thread pool funcionar como
esperado. Mais pra frente, quando eu tiver um tempo, vou tentar
trabalhar com ele novamente, com certeza.
Reply all
Reply to author
Forward
0 new messages