BT

Programando com a biblioteca assíncrona do Scala

| por Rafael Salerno Seguir 1 Seguidores em 25 nov 2016. Tempo estimado de leitura: 8 minutos |

Um dos conceitos que pode ser considerado chave para a escalabilidade é a programação assíncrona.

A programação assíncrona consiste em executar qualquer tarefa mais pesada, em termos de recursos computacionais, em um processo ou máquina separada, fora do workflow natural de um aplicativo.

Em uma requisição assíncrona, não existe sincronismo entre as requisições, o que ocorre é o envio de diversas requisições em paralelo. A programação assíncrona permite a delegação de processos de aplicação para outros tópicos, sistemas e/ou dispositivos. Programas síncronos são executados de forma sequencial, enquanto aplicativos assíncronos podem iniciar uma nova operação sem esperar a conclusão das novas operações.

Exemplo

Dado um projeto que permite aos usuários o envio de e-mails e que não podem fazer nenhuma outra tarefa até que a resposta do remetente seja recebida. Deste modo, qualquer outra tarefa está associada ao término do processo de envio e recebimento do e-mail, independente se demorar muito tempo ou não. A proposta do uso de uma tarefa é permitir o envio do e-mail e continuar utilizando as demais tarefas do sistema enquanto espera a resposta do remetente.

Quando uma requisição web chega ao servidor, a aplicação precisa executar vários processos até montar o HTML de resposta. Esse tempo precisa ser o menor possível, permitindo que o servidor atenda a uma maior quantidade de requisições. Se uma requisição demora mais, porque precisa ficar esperando operações de leitura e escrita (I/O), como salvar um registro no banco de dados, executar uma query pesada, chamar um web service, entre outros, pode-se dizer que a requisição é I/O bound.

Contudo, se a requisição demora mais por processamento, por exemplo, para processar um array de objetos, realizar transformação de uma estrutura em outra, gerar um PDF ou montar um HTML, pode-se dizer que a requisição tem um CPU bound.

Dado os dois tipos possíveis de gargalos, no caso do CPU bound, a única maneira de resolver é ter CPUs mais rápidos ou CPUs em paralelo. A aplicação precisa suportar multithread ou multi processos para utilizar todas as CPUs. No caso do I/O bound, o sistema precisa suportar chamadas assíncronas. O sistema terá chamadas de notificações de eventos que informarão quando, por exemplo, uma chamada ao banco de dados foi concluída.

O Scala tem uma abordagem em sua API de Concurrency muito simples para implementar a ideia de programação assíncrona. Usando essa API de Concurrency, o compilador faz o trabalho difícil e o aplicativo mantém a estrutura lógica que se assemelha ao código síncrono. Como resultado, obtém-se todas as vantagens da programação assíncrona com pouco esforço.

Um dos benefícios dessa assincronicidade é que as aplicações ficam com uma grande possibilidade de escalabilidade. Permitindo manipular uma porção crescente de trabalho de forma uniforme, deixando o sistema preparado para crescer ou se tornar distribuída com pouco esforço.

Future/Promise em Scala

Future é uma API de Scala que proporciona uma maneira de executar operações em paralelo de forma não bloqueante. O Future é um objeto que possui um espaço reservado para trabalhar com a espera de um resultado que ainda não possui. Com isso, pode-se compor tarefas simultâneas de forma assíncrona e sem bloqueio de uma forma rápida. Em Scala é possível combinar o Future com maps, for-comprehensions e filters de modo não bloqueante e imutável. Definindo Future, pode-se dizer que é um objeto que contém um valor que pode se tornar disponível.

Um Future pode estar completo ou concluído de duas formas:

  • Concluído com sucessos e tem valor;
  • Concluído com falha e possui uma exceção como valor.

Exemplo:

import scala.concurrent._
import ExecutionContext.Implicits.global
val f : Future[List[User]] =  future{
  dao.getUsers()
}

Neste exemplo, a importação do contexto de execução global padrão do Scala é feita por meio do import ExecutionContext.Implicits.global, que fornece pools de threads para lidar com processos assíncronos.

Em seguida, há uma chamada hipotética ao banco de dados, sabendo que isso pode levar algum tempo, é possível fazer a chamada de modo assíncrono para não bloquear as demais threads. Quando o processo estiver pronto a resposta estará disponível na variável f.

Callbacks

Para interagir com os valores do Future é necessário associar a variável do Future a um callback. Esse callback é chamado de forma assíncrona quando o Future for concluído. Se o Future foi concluído, o registro é associado a um callback e o retorno pode tanto ser executado de forma assíncrona ou sequencial. A forma mais comum de registrar um callback é usando o método onComplete, que aplica seu resultado: a um Success, se foi concluído com êxito e Failure se for concluído com uma exceção.

Exemplo:

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.Success
import scala.util.Failure

case class User(name: String, age: Int)

object TestFuture {
  val f: Future[List[User]] = future {
   dao.getUsers() 
  }

  f onComplete {
    case Success(result) => result.map(f=> println("Users: "+f.name))
    case Failure(t) => println("Ocorreu um erro no Future: " + t.getMessage)
  }
}

Os métodos onComplete, Success e Failure são do tipo Unit. Logo, esses métodos não podem ser encadeados com outros. Porém, tudo que é executado dentro desse métodos têm escopo restrito a somente ele.

Promises

Pode-se definir os Futures com objetos de somente leitura, em que se trabalha com um resultado de métodos que pode vir a acontecer.

O Promisse pode ser pensado como um objeto ou um recipiente que será atribuído o valor de quando o Future tiver um resultado.Por padrão, um Promise completo retorna um Future. Quando se completar uma promessa, seja por falha ou sucesso, poderá ser acionado todo o comportamento que foi anexado ao futuro associado.

Exemplo:

import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.util.Success
import scala.util.Failure

case class User(name: String, age: Int)

object TestFuture {
  val f: Future[List[User]] = future {
    dao.getUsers() 
  }

  val resultPromise: Promise[List[User]] = Promise[List[User]]

  f onComplete {
    case Success(result) => resultPromise.success(result)
    case Failure(t) => resultPromise.failure(t)
  }

  resultPromise.future.map { f =>
    f.map(user => println("value = " + user.name))
  }
}

Neste exemplo, a variável resultPromises é literalmente a promessa que se terá uma Lista de objetos User consultados do banco de dados; um Success é atribuído o valor do Future com sucesso para o Promise; um Failure é atribuído o valor de falha ao Promise

O Promise continuará devolvendo um Future, mas uma das formas de interagir com esse Future é utilizando um map. Com um map, acessa-se os valores do Future e com o segundo map, percorre-se a Lista de Usuários.

For-comprehensions/Maps/FlatMap

Interagir utilizando Futures com callback e Promises é simples, mas exige que se escreva muito código. Quando na verdade, quer-se alguma forma mais rápida e simples. A combinação com outras funções do Scala permite um melhor uso dos Futures. O for-comprehension é uma das maneiras de interagir com funções que estão dentro de um Future. Dentro dele pode-se avaliar processamentos em paralelo e no final agregar em um só resultado.

Exemplo:

val result1:Future[List[User]] = future(dao.getUsers())
val result2:Future[List[People]] = future(dao.getPeople())

val res = for {
  r1 <- result1
  r2 <- result2
} 

yield (r1+r2)

Também é possível usar a função de Map para interagir com processamentos que estão dentro de um Future.

Durante a escrita da função que é passada para o map, tem se um futuro possível. Essa função de mapeamento é executada assim que o seu futuro [de List] for concluído com êxito.

No entanto, dessa forma pode ser esperado apenas o resultado de sucesso de uma lista de números. Caso essa lista venha a falhar, será recebido um Future[List] com uma falha, o mesmo caso pode-se considerar no exemplo anterior com o for-comprehension.

Exemplo:

var listNumbers:Future[List] = Future(List(1,2,3,4,5))

listNumbers.map(list=> list.map(number=> println(number)))
resultado = 1234 

Se o cálculo de um Future depende do resultado de outro Future, pode-se provavelmente recorrer ao flatMap para evitar uma estrutura profundamente aninhada de futuros. O FlatMap funciona aplicando uma função que retorna uma sequência para cada elemento da lista, alinhando os resultados na lista original. Tem a grande vantagem de fazer o mesmo com funções que estão dentro de um Future.

Exemplo:

val f1 = Future( "Hello" + "World" )

val f2 = Future(3)

val f3 = f1.flatMap(x ⇒ f2.map (y => x.length * y))

f3.value = Some(Success(30))

Conclusão

Usar o conceito de programação assíncrona é certamente um benefício. Um conceito simples como um gerenciador de fila de processos pode mudar totalmente a forma como se desenvolve aplicativos. É possível pensar que os Futures são os produtores e Promises são os consumidores. Se a fila está grande e os jobs estiverem acumulando, pode-se adicionar mais consumidores, pois eles já estão configurados para consumir uma fila. Future é essencial para uma referência read-only para um valor que ainda deve ser processado. Promise é praticamente o mesmo, exceto que pode-se escrever nele.

Em resumo, em ambos é possível realizar leitura, contudo, apenas o Promise pode escrever. A principal vantagem de Future (programação assíncrona), é a tarefa de atuar em segmentos diferentes, por isso a thread principal não fica bloqueada até a tarefa ser concluída. É possível executar outras tarefas ao mesmo tempo. O modelo assíncrono em caso de tarefa de longa duração é muito útil, para que o seu aplicativo reaja a outras ações realizadas pelo usuário.

Referências:

Sobre o autor:

Screen Shot 2016-11-06 at 23.36.41.pngAtualmente Rafael Salerno de Oliveira trabalha como Arquiteto de Software na ilegra em Porto Alegre, RS. Ele possui mais de 10 anos de experiência trabalhando com desenvolvimento, design e arquitetura de software. Nos últimos anos vem trabalhando em diferentes projetos incentivando a utilização de Programação Funcional, eXtreme Programming, Kanban, Arquitetura Evolucionária, Integração Contínua, Entrega Contínua, TDD, SOA e Microservices.

Avalie esse artigo

Relevância
Estilo/Redação

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

Dê sua opinião

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão
Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

Dê sua opinião

Faça seu login para melhorar sua experiência com o InfoQ e ter acesso a funcionalidades exclusivas


Esqueci minha senha

Follow

Siga seus tópicos e editores favoritos

Acompanhe e seja notificados sobre as mais importantes novidades do mundo do desenvolvimento de software.

Like

Mais interação, mais personalização

Crie seu próprio feed de novidades escolhendo os tópicos e pessoas que você gostaria de acompanhar.

Notifications

Fique por dentro das novidades!

Configure as notificações e acompanhe as novidades relacionada a tópicos, conteúdos e pessoas de seu interesse

BT