Concurrencia y Paralelismo

Programación concurrente con Futures y Promises

La programación concurrente en Scala se facilita mediante el uso de Futures y Promises, que permiten ejecutar tareas de manera asíncrona y gestionar resultados futuros de forma controlada. A continuación, te explicaré cómo funcionan y cómo se utilizan en Scala.

En Scala, los Futures representan cálculos asíncronos que pueden completarse en el futuro, mientras que Promises son constructores manuales de Futures, que permiten controlar cuándo y cómo se completa un Future.

				
					import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Future, Promise}
import scala.util.{Success, Failure}

// Función que simula una operación asíncrona
def calcularResultado(): Future[Int] = {
  val promise = Promise[Int]()
  
  // Simulando un cálculo asíncrono
  val thread = new Thread(() => {
    Thread.sleep(1000) // Simula un tiempo de espera
    val resultado = 42 // Resultado del cálculo
    promise.success(resultado) // Completa el promise con el resultado
    // Si hubiera un error: promise.failure(new RuntimeException("Error"))
  })
  thread.start()
  
  promise.future // Retorna el Future asociado al Promise
}

// Uso del Future para obtener el resultado
val resultadoFuturo: Future[Int] = calcularResultado()

// Manejo del resultado del Future
resultadoFuturo.onComplete {
  case Success(resultado) => println(s"Resultado obtenido: $resultado")
  case Failure(excepcion) => println(s"Se produjo un error: ${excepcion.getMessage}")
}

// Esperar a que el Future se complete (solo para fines demostrativos)
Thread.sleep(2000)

				
			

Explicación del Código

  • Importación de Librerías:

    • scala.concurrent.ExecutionContext.Implicits.global proporciona un contexto de ejecución global para los Futures.
    • scala.concurrent.{Future, Promise} y scala.util.{Success, Failure} son las librerías necesarias para trabajar con Futures y manejar resultados exitosos o errores respectivamente.
  • Función calcularResultado():

    • calcularResultado(): Future[Int] simula una operación asíncrona. Dentro de ella, se crea un Promise[Int] que se completará manualmente más adelante con un resultado (42 en este caso después de una simulación de espera).
  • Creación de un Thread para Simular Asincronía:

    • Se crea un nuevo hilo (Thread) donde se realiza el cálculo. Este hilo espera un segundo (Thread.sleep(1000)) antes de completar el Promise con el resultado deseado.
  • Uso de Future:

    • calcularResultado() retorna el Future asociado al Promise, que es donde se podrá obtener el resultado una vez que esté disponible.
  • Manejo del Resultado del Future:

    • resultadoFuturo.onComplete { ... } permite definir acciones que se realizarán cuando el Future se complete, ya sea con éxito (Success(resultado)) o con un error (Failure(excepcion)).
  • Espera del Resultado (solo para demostración):

    • Thread.sleep(2000) se utiliza aquí solo para esperar el tiempo suficiente para que el Future se complete en el ejemplo, aunque en aplicaciones reales no se debe usar esta técnica para esperar Futures.

Conclusiones

  • Los Futures en Scala permiten ejecutar operaciones asíncronas de manera eficiente y manejar sus resultados de forma concurrente.
  • Los Promises son útiles para controlar y completar manualmente Futures con resultados.
  • El manejo de Futures con onComplete permite manejar de manera clara los resultados exitosos y errores potenciales de las operaciones asíncronas.

Akka y actores en Scala

Akka es un conjunto de herramientas y librerías para construir aplicaciones concurrentes, distribuidas y tolerantes a fallos en Scala y Java. Los actores en Akka son entidades de programación concurrente que procesan mensajes de forma asincrónica y encapsulan estado y comportamiento.

Código

Para implementar un sistema de actores básico con Akka en Scala, necesitamos seguir algunos pasos fundamentales:

  1. Definir el Actor: Crear una clase que extienda Actor y defina cómo manejará los mensajes que reciba.

  2. Crear el Sistema de Actores: Configurar y crear un ActorSystem, que es el entorno donde residen y se ejecutan los actores.

  3. Enviar Mensajes a los Actores: Enviar mensajes a los actores para que procesen.

Aquí tienes un ejemplo:

				
					import akka.actor._

// Definición del Actor
class MiActor extends Actor {
  def receive: Receive = {
    case "saludar" => println("Hola desde el actor!")
    case mensaje: String => println(s"Mensaje recibido: $mensaje")
    case numero: Int => println(s"Número recibido: $numero")
    case _ => println("Mensaje no reconocido")
  }
}

// Creación del ActorSystem
val system = ActorSystem("SistemaActores")

// Creación de un actor dentro del ActorSystem
val miActor = system.actorOf(Props[MiActor], "miActor")

// Envío de mensajes al actor
miActor ! "saludar"
miActor ! "Hola actor, ¿cómo estás?"
miActor ! 42
miActor ! 3.14

// Detener el ActorSystem (solo para fines de demostración)
system.terminate()

				
			

Explicación del Código

  • Importación de Akka: Akka se importa mediante akka.actor._, proporcionando las clases y métodos necesarios para trabajar con actores.

  • Definición del Actor (MiActor): La clase MiActor extiende Actor e implementa el método receive, que define cómo el actor manejará los mensajes que recibe. En este caso, imprime diferentes tipos de mensajes recibidos.

  • Creación del ActorSystem: ActorSystem es creado con ActorSystem("SistemaActores"). Es el entorno donde residen y se gestionan los actores.

  • Creación del Actor (miActor): system.actorOf(Props[MiActor], "miActor") crea un actor de tipo MiActor dentro del ActorSystem.

  • Envío de Mensajes: Los mensajes se envían al actor usando el operador !. El actor procesa los mensajes de manera asincrónica según su implementación en receive.

  • Terminación del ActorSystem: system.terminate() se utiliza para detener el ActorSystem una vez que se han enviado todos los mensajes. En una aplicación real, esto se manejaría de manera adecuada según los requisitos del sistema.

Conclusiones

  • Akka proporciona un modelo de actores para manejar la concurrencia de manera eficiente y segura en Scala.
  • Los actores en Akka son unidades de ejecución asincrónica que encapsulan estado y procesan mensajes de forma independiente.
  • El ejemplo proporcionado muestra cómo crear, enviar mensajes y manejar actores básicos utilizando Akka en Scala.

Paralelismo con Parallel Collections

En Scala, las Parallel Collections (colecciones paralelas) permiten realizar operaciones en paralelo de manera automática sobre elementos de una colección. Esto facilita el procesamiento eficiente de datos distribuidos en entornos multicore.

Código Completo

				
					// Importación de las colecciones paralelas
import scala.collection.parallel.CollectionConverters._

// Ejemplo de uso de Parallel Collections
object ParalelismoConParallelCollections extends App {

  // Creación de una lista de números
  val numeros = (1 to 1000000).toList

  // Conversión a Parallel Collection
  val parallelNumeros = numeros.par

  // Operación de suma en paralelo
  val suma = parallelNumeros.map(_ * 2).sum

  // Impresión del resultado
  println(s"La suma de los números multiplicados por 2 es: $suma")
}

				
			

Explicación del Código

  • Importación de Parallel Collections: scala.collection.parallel.CollectionConverters._ permite convertir colecciones estándar en colecciones paralelas.

  • Creación de la Lista de Números: val numeros = (1 to 1000000).toList crea una lista de números del 1 al 1,000,000.

  • Conversión a Parallel Collection: val parallelNumeros = numeros.par convierte la lista numeros en una Parallel Collection (ParSeq en este caso).

  • Operación en Paralelo: parallelNumeros.map(_ * 2).sum realiza la multiplicación por 2 de cada elemento en paralelo y luego suma todos los resultados.

  • Impresión del Resultado: Se imprime el resultado de la suma.

Aspectos Clave

  • Ventajas del Paralelismo: Las Parallel Collections permiten aprovechar automáticamente el paralelismo disponible en el hardware multicore, mejorando el rendimiento de operaciones intensivas en datos.

  • Métodos Paralelos: Métodos como map, filter, reduce, sum, entre otros, se ejecutan automáticamente en paralelo cuando se utilizan con Parallel Collections.

  • Conversión y Uso: La conversión a Parallel Collection se realiza con el método .par en cualquier colección estándar (List, Vector, Range, etc.).

  • Consideraciones: Es importante tener en cuenta que el paralelismo introduce complejidades adicionales como la sincronización y el uso eficiente de recursos, por lo que se debe evaluar y medir el rendimiento en contextos específicos.