Equipe: Gabriela Cunha (gcs@cin.ufpe.br), Roberto Souto Maior (rsmbf@cin.ufpe.br)
Tema
Incluir mecanismo de concorrência, inspirado no de Erlang, na linguagem Funcional 2
Motivação
Concorrência é a habilidade de diferentes funções executarem em paralelo, sem afetar umas as outras, a não ser quando programadas explicitamente para isso. Cada atividade concorrente em Erlang é chamada de processo. A única forma de processos interagirem uns com os outros é através de troca de mensagens, onde dados são enviados de um processo para outro.
O modelo de concorrência de Erlang foi construído desde o seu início. Com processos leves, é usual ter centenas de milhares, ou até milhões, de processos executando em paralelo, e ainda assim sem muito consumo de memória. A habilidade do sistema de execução de escalar concorrência a esses níveis afeta diretamente a forma como os programas são desenvolvidos, diferenciando Erlang de outras linguagens concorrentes.
Muitas empresas usam Erlang em seus sistemas, principalmente por conta da escalabilidade do seu mecanismo de concorrência:
- A Amazon usa Erlang para implementar o SimpleDB, provendo serviços de banco de dados como parte do Amazon Elastic Compute Cloud (EC2).
- A Yahoo! usa em seu serviço social de favoritos, Delicious, que contém mais de 5 milhões de usuários e 150 milhões de URLs.
- O Facebook utiliza Erlang para dar suporte ao servidor de seu serviço de chat, lidando com mais de 100 milhões de usuários ativos.
- A T-Mobile usa Erlang nos sistemas de SMS e autenticação.
- A Motorola usa Erlang em produtos de processamento de chamadas.
Descrição
Em Erlang um processo pode ser criado através de duas variações da função spawn:
- spawn(Fun) - Recebe uma função como parâmetro que irá ser executada de maneira isolada no novo processo.
- spawn(Mod, Fun, Params) - Recebe três parâmetros: o módulo onde se encontra a função a ser executada, a função a ser executada e seus argumentos.
Como mencionado anteriormente, processos se comunicam através de troca de mensagens, onde o conteúdo das mensagens pode ser qualquer estrutura de dados da linguagem. Cada processo tem uma caixa de mensagens associada ao mesmo. A medida que um processo recebe novas mensagens elas chegam à caixa de mensagens e são organizadas em uma fila, isto é, a primeira mensagem que chega será a primeira que se tentará ler.
Mensagens são enviadas utilizando a estrutura Id ! Msg, onde Id é o identificador do processo que deve receber a mensagem, ! é o operador de envio de mensagem e Msg é a mensagem que deve ser enviada para o processo Id.
Para receber uma mensagem, é utilizada a estrutura receive. Esta estrutura utiliza casamento de padrão para mapear o conteúdo das mensagens e tem o seguinte formato:
Pattern2 when Guard2 -> exp21, .., exp2n;
...
Other -> expn1, .., expnn
- Se houver sucesso no casamento de padrão, a mensagem é recuperada da caixa de mensagens, as variáveis no padrão são atribuídas as partes correspondentes da mensagem e o corpo da cláusula em questão é executado.
- Se nenhuma das cláusulas casar, as mensagens subsequentes são casadas em ordem com todas as cláusulas até uma delas casar ou todas tenham falhado em todos os possíveis padrões.
Exemplo
O exemplo acima contém um módulo, chamado echo, com 2 funções: go() e loop(). Ao chamarmos a função go(), um processo é criado utilizando a função spawn. Este processo irá executar a função loop() de maneira concorrente a execução de go(), que por sua vez se dará no processo inicial. Como parâmetros na criação do processo foram passados o módulo echo, a função loop() e lista vazia, pois a função loop() não recebe nenhum parâmetro. O processo responsável por executar loop() será inicialmente suspenso, porque esta função contém uma cláusula receive, mas sua caixa de mensagens está vazia.
O processo inicial, ainda executando em go(), usa o Id (Pid) retornado pela chamado de spawn, para enviar uma mensagem para o processo que está executando loop(). Esta mensagem contém uma tupla com o Id do processo, retornado por self(), e uma string "Hello". A função self(), é uma função pré-definida responsável por retornar o Id do processo sendo executado no momento. Após enviar a mensagem, este processo será suspenso em sua cláusula receive, dado que sua caixa de mensagens está vazia.
Como o processo responsável por executar loop() recebe uma mensagem, ele é retomado, e tenta, em ordem, casar a mensagem recebida com alguma das suas cláusulas. Neste exemplo, o padrão {From, Msg} irá casar com a mensagem recebida e o corpo desta cláusula será executado. Ou seja, este processo irá enviar uma resposta para o processo inicial com com uma tupla contendo o seu Id, retornado por self(), e o conteúdo da mensagem recebida inicialmente, "Hello", concatenado com " world!". Finalmente, a execução deste processo irá chamar a função loop() recursivamente, para evitar que a execução do processo termine. A execução recursiva desta função, irá fazer com que a função loop() trave novamente na cláusula receive.
Em paralelo, o processo inicial irá ter recebido a mensagem, enviada pelo processo executando loop(), e poderá retomar sua execução. A mensagem recebida irá casar com o único padrão de sua cláusula receive e portanto o seu corpo poderá ser executado, levando a chamada de io:format() com o conteúdo da mensagem recebida ("Hello world!"). Em seguida, a execução desta função envia o booleano false para o outro processo.
Finalmente, a mensagem recebida pelo segundo processo casa com o segundo padrão da cláusula receive de loop(), e retorna true, terminando normalmente.
Referências
- Cesarini, Francesco, and Simon Thompson. Erlang programming. " O'Reilly Media, Inc.", 2009.
- Hébert, Fred. Learn You Some Erlang for Great Good!: A Beginner's Guide. No Starch Press, 2013. Disponível em: http://learnyousomeerlang.com/content