Modelo de Atores em Scala: Conceitos Fundamentais e Práticas de Concorrência

Introdução ao Modelo de Atores

O Scala oferece um modelo de concorrência baseado em atores, onde cada ator é uma entidade semelhante a uma thread que possui uma caixa de mensagens privada. Os atores se comunicam exclusivamente através de mensagens assíncronas, eliminando a necessidade de gerenciamento explícito de locks e sincronização.

  1. Utilização Básica de Atores

O Scala gerencia um pool de threads que é compartilhado por todos os atores do sistema. Existem duas abordagens principais para o processamento de mensagens:

O modelo receive remove uma thread do pool e a utiliza exclusivamente até que o processamento seja concluído. Já o modelo react reutiliza a thread após o processamento, permitindo que outro ator a utilize, o que resulta em melhor eficiência.

Exemplo 1: Definição de Ator via Herança

import scala.actors.Actor

class AtorSimples extends Actor {
  def act() {
    for (i <- 1 to 5) {
      println("Execução número: " + i)
      Thread.sleep(1000)
    }
  }
}

object ExemploAtor {
  def main(args: Array[String]): Unit = {
    val ator = new AtorSimples()
    ator.start()
  }
}

Saída:

Execução número: 1 Execução número: 2 Execução número: 3 Execução número: 4 Execução número: 5

Exemplo 2: Criação de Ator com Método factory

import scala.actors.Actor

object ExemploAtor2 {
  def main(args: Array[String]): Unit = {
    Actor.actor {
      for (i <- 1 to 5) {
        println("Outro Ator " + i)
        Thread.sleep(1000)
      }
    }
  }
}

Saída:

Outro Ator 1 Outro Ator 2 Outro Ator 3 Outro Ator 4 Outro Ator 5

  1. Comunicação entre Atores

Operadores de Envio de Mensagens

Operador Descrição
! Envia mensagem assíncrona sem retorno.
!? Envia mensagem síncrona e bloqueia até receber resposta.
!! Envia mensagem assíncrona retornando Future[Any].
? Sem parâmetros. Retorna próxima mensaegm da caixa.
  1. Recebimento de Mensagens

3.1 Receive: Processamento Repetido

O receive mantém a thread ocupada enquantoprocessa mensagens. Para processar múltiplas mensagens, utiliza-se um loop externo.

import scala.actors.Actor

val atorProcessador = Actor.actor {
  var ativo = true
  while (ativo) {
    Actor.receive {
      case numero: Int => println("Recebi inteiro: " + numero)
      case texto: String => println("Recebi texto: " + texto)
      case _ => println("Tipo de mensagem não reconhecido")
    }
  }
}

atorProcessador ! 42
atorProcessador ! 100
atorProcessador ! 3.14

Saída:

Recebi inteiro: 42 Recebi inteiro: 100 Tipo de mensagem não reconhecido

3.2 React: Reutilização de Threads

O react nunca retorna e permite reutilização de threads, sendo mais eficiente que receive. Utiliza-se loop para processar mensagens repetidamente.

import java.net.InetAddress
import java.net.UnknownHostException
import scala.actors.Actor

object ServicoDNS {
  def main(args: Array[String]): Unit = {
    ResolvedorNome.start()
    ResolvedorNome ! ("www.google.com", Actor.self)
    ResolvedorNome ! "mensagem_teste1"
    ResolvedorNome ! "SAIR"
    ResolvedorNome ! "mensagem_teste2"
  }
}

object ResolvedorNome extends Actor {
  def act() {
    loop {
      react {
        case (nome: String, ator: Actor) =>
          println(obterIP(nome))
        case "SAIR" =>
          println("Resolvedor encerrando...")
          exit()
        case msg =>
          println("Mensagem não tratada: " + msg)
      }
    }
  }

  def obterIP(nome: String): Option[InetAddress] = {
    try {
      Some(InetAddress.getByName(nome))
    } catch {
      case _: UnknownHostException => None
    }
  }
}

Saída:

Some(www.google.com/142.250.190.46)
Mensagem não tratada: mensagem_teste1
Resolvedor encerrando...

  1. Práticas Recomendadas

4.1 Evitar Bloqueio em Atores

Atores não devem bloquear durante o processamento de mensagens. Quando um ator bloqueia, ele não consegue responder a outras requisições, podendo causar deadlock em cenários com múltiplos atores.

object DemoNaoBloqueante {
  
  def executarTarefaAssincrona(atorPrincipal: Actor) {
    Actor.actor {
      Thread.sleep(2000)
      atorPrincipal ! "TarefaConcluida"
    }
  }
  
  def main(args: Array[String]): Unit = {
    val ator = Actor.actor {
      var execucoes = 0
      var continuar = true
      
      executarTarefaAssincrona(Actor.self)
      
      Actor.loopWhile(continuar) {
        Actor.react {
          case "TarefaConcluida" =>
            println("Executando operação " + execucoes)
            execucoes += 1
            if (execucoes < 3) {
              executarTarefaAssincrona(Actor.self)
            } else {
              continuar = false
            }
          case msg =>
            println("Recebido: " + msg)
        }
      }
    }
  }
}

Saída:

Executando operação 0 Executando operação 1 Executando operação 2

4.2 Comunicação Exclusiva por Mensagens

Atores devem comunicar-se apenas através de mensagens, nunca compartilhando referências diretas. Compartilhar referências permite acesso concorrente a dados mutáveis, reintroduzindo problemas de sincronização que o modelo de atores busca evitar.

4.3 Utilizar Mensagens Imutáveis

Como cada ator executa em uma única thread, não há preocupações com thread-safety dentro do método act. Porém, objetos enviados como mensagens são compartilhados entre atores, exigindo atenção.

A melhor abordagem é utilizar exclusivamente objetos imutáveis como mensagens. Classes case em Scala são ideais para isso, pois seus campos são vals por padrão. Utilize tipos básicos, String, Tuplas, List, Set e Map imutáveis.

Quando necessário enviar objetos mutáveis, envie uma cópia defensiva:

// Enviar cópia ao invés de referência direta
atorDestino ! arrayOriginal.clone()

// Ou converter para coleção imutável
atorDestino ! arrayOriginal.toList

4.4 Mensagens Autoexplicativas

Crie classes case específicas para cada tipo de mensagem ao invés de usar tuplas genéricas:

// Abordagem menos clara
consultorDNS ! ("www.exemplo.com", self)

// Abordagem mais expressiva
case class ConsultaDNS(hostname: String, solicitante: Actor)
consultorDNS ! ConsultaDNS("www.exemplo.com", self)

  1. Comunicação Entre Diferentes JVMs

Servidor:

import scala.actors.Actor
import scala.actors.remote.RemoteActor
import scala.actors.remote.Node

object ServidorRemoto {
  def main(args: Array[String]): Unit = {
    Actor.actor {
      RemoteActor.alive(3000)
      RemoteActor.register('servicoPrincipal, Actor.self)
      
      Actor.loop {
        Actor.receive {
          case msg =>
            println("Servidor recebeu: " + msg)
        }
      }
    }
  }
}

Cliente:

import scala.actors.Actor
import scala.actors.remote.RemoteActor
import scala.actors.remote.Node

object ClienteRemoto {
  def main(args: Array[String]): Unit = {
    Actor.actor {
      val no = Node("127.0.0.1", 3000)
      val atorRemoto = RemoteActor.select(no, 'servicoPrincipal)
      
      println("-- Iniciando envio de mensagem")
      atorRemoto ! "Mensagem do cliente"
      println("-- Envio concluído")
    }
  }
}

Saída no servidor:

Servidor recebeu: Mensagem do cliente

Tags: Scala actor-model Concurrency thread parallel-programming

Publicado em 8-4 10:11