Lenguajes
Ada
Historia: Ada es un lenguaje nacido de un proyecto en los 70s del ejército de los Estados Unidos, con la principal prioridad en la seguridad y la minimización de errores.
Concurrencia en el lenguaje: Sincroniza las tareas por rendez vous.
Herramientas para aprovechar la concurrencia: Amplio uso en la industria de las infraestructuras de riesgo grande, como sistemas de aviones, trenes, tanques y misiles.
Aplicaciones notables: Sistemas de planeación aérea y naval.
Haskell
Historia: Haskell es un lenguaje polimórficamente tipificado, perezoso, puramente funcional, muy diferente a la mayoría de los otros lenguajes de programación.
Concurrencia en el lenguaje: La concurrencia es "ligera", lo que significa que tanto la creación de hilos como los gastos generales de cambio de contexto son extremadamente bajos.
Herramientas para aprovechar la concurrencia: La programación de los hilos de Haskell se hace internamente y no hace uso de ningún paquete de hilos suministrado por el sistema operativo.
Aplicaciones notables: Análisis de sintaxis en código alojado en GitHub.
Erlang
Historia: Creado por Ericsson en 1986 con el propósito de desarrollar sistemas de telecomunicaciones. Diseñado para soportar numerosos procesos concurrentes. Liberado como software libre en 1998. A llevado al desarrollo de otros lenguajes que priorizan la concurrencia.
Concurrencia en el lenguaje: Modelo de concurrencia basado en actores, esto quiere decir que cuenta con procesos aislados que no comparten memoria, estos procesos se comunican a través de mensajes y son procesos ligeros gestionados por la máquina virtual BEAM.
Herramientas para aprovechar la concurrencia: Erlang ofrece diversas herramientas y bibliotecas que facilitan la gestión de la concurrencia y la construcción de sistemas distribuidos. La principal de estas es OTP (Open Telecom Platform) que proporciona componentes como supervisores, gen_servers, y otros patrones de diseño que facilitan la gestión de la concurrencia y la tolerancia a fallos.
- Supervisores: Procesos especiales que supervisan otros procesos
- Gen_servers: Gestionan el ciclo de vida de los procesos y la comunicación entre ellos.
Aplicaciones notables: Lógica de negocio de aplicaciones como WhatsApp.
Elixir
Historia: Elixir es un lenguaje de programación de propósito general, concurrente y funcional. Comenzó en 2011 con el objetivo de aprovechar la robustez y el rendimiento de Erlang y su máquina virtual, introduciendo características y mejoras que hicieran el lenguaje más productivo para los desarrolladores modernos, lo que lo ha hecho popular en el entorno de desarrollo web.
Concurrencia en el lenguaje: Al igual que Erlang, Elixir se basa en el modelo de actores y aprovecha la máquina virtual BEAM, ofreciendo aislamiento de procesos, comunicación mediante mensajes y la ligereza de los procesos.
Herramientas para aprovechar la concurrencia: Además de las herramientas OTP, podemos encontrar Phoenix Web Framework, capaz de manejar un alto tráfico concurrente y adecuado para aplicaciones con notificaciones en tiempo real; Task, para ejecutar funciones de forma concurrente; y Agent, para gestionar el estado compartido entre procesos.
Aplicaciones notables: Changelog, una plataforma de podcasts para desarrolladores.
Rust
Historia: Rust es un lenguaje de programación compilado, de propósito general y multiparadigma. Patrocinado por Mozilla desde 2009, Rust tiene como propósito ofrecer seguridad en memoria y concurrencia, diseñado específicamente para evitar problemas comunes como condiciones de carrera, desbordamiento de búfer y violaciones de acceso a memoria.
Concurrencia en el lenguaje: En el modelo de propiedad y préstamos de Rust, la propiedad asegura que solo un propietario pueda modificar un valor a la vez, mientras que los préstamos permiten el acceso seguro a datos compartidos. En cuanto a los tipos de concurrencia, Rust soporta concurrencia basada en hilos (threads) y basada en tareas asíncronas. Además, proporciona seguridad en la concurrencia al verificar las condiciones de seguridad en tiempo de compilación.
Herramientas para aprovechar la concurrencia: En Rust, std::thread permite crear y gestionar hilos de manera segura, mientras que std::sync proporciona primitivas de sincronización para manejar el acceso concurrente a los datos. Además, las funciones async/await facilitan la escritura de código asíncrono y concurrente.
Aplicaciones notables: Partes del kernel de Linux, cuya escritura es soportada desde octubre de 2022.
GO
Historia: Diseñado en Google en 2007, Go (también conocido como Golang) tenía como objetivo combinar lo mejor de los lenguajes compilados, como la eficiencia y la seguridad, con las ventajas de los lenguajes interpretados, como la facilidad de uso y la productividad. Hoy en día, Go es popular en sistemas distribuidos, microservicios y aplicaciones en la nube
Concurrencia y herramientas en el lenguaje: En Go, las goroutines son funciones que se ejecutan concurrentemente en el mismo espacio de direcciones y son gestionadas por el runtime de Go. Los canales permiten la comunicación y sincronización entre goroutines, pudiendo ser sincrónicos o asincrónicos.
Aplicaciones notables: El código backend de Uber.
Crystal
Historia: Crystal es uno de los nuevos lenguajes en la escena, iniciado en 2012. Tiene una sintaxis similar a Ruby, estáticamente tipado, compilado y es self-hosted (Crystal está escrito en Crystal).
Concurrencia en el lenguaje: Crystal viene con una primitiva de concurrencia llamada fibras, que son básicamente una versión más ligera de hilos. Las otras primitivas de concurrencia son canales.
Herramientas para aprovechar la concurrencia: Utiliza canales para la concurrencia, similar a Go.
Aplicaciones notables: Servicios web de compañías como Errordeck, GigSmart, Appmonitor, etc.
Java
Historia: Java es un lenguaje de programación orientado a objetos creado en 1991 y publicado en 1995 por Sun Microsystem (adquirida por Oracle en 2010), con la intención de que los programadores escribieran el código solo una vez y lo ejecutaran en cualquier dispositivo.
Concurrencia en el lenguaje: Java permite realizar concurrencia con la librería threads. Además, cuenta con otras librerías que implementan métodos de planificación y control de procesos (variables atómicas, semáforos, entre otros) traídos del paquete concurrent.
Aplicaciones notables: Innumerables. Desde servicios web backend mediante Spring Boot hasta desarrollo de videojuegos con LWJGL, la librería usada para escribir Minecraft.
Python
Historia: Python es el lenguaje más utilizado a nivel mundial (según revista IEEE). Es un lenguaje de sintaxis simple y cubre una gran cantidad de áreas del conocimiento como inteligencia artificial y ciencia de datos.
Concurrencia en el lenguaje: Cuenta con varias librerías para la implementación de programación concurrente y su respectivo control, como lo son la librería "Thread" y "Multiprocessing".
Herramientas para aprovechar la concurrencia: La librería "Thread" incluye una interfaz de alto nivel orientada a objetos para trabajar con concurrencia desde Python. Los objetos Thread se ejecutan al mismo tiempo dentro del mismo proceso y comparten memoria.
Aplicaciones notables: El backend de Instagram.
C
Historia: C es uno de los lenguajes más rápidos que existen puesto a que es un lenguaje compilado. Es altamente usado en el diseño y desarrollo de los sistemas operativos que hoy en día se usan.
Concurrencia en el lenguaje: En cuanto a programación concurrente, usa la librería pthread.h para la creación de hilos y métodos de control muy básicos como la implementación de un semáforo.
Aplicaciones notables: El kernel de Linux y otros lenguajes como Python y C++.
C++
Historia: Fue diseñado a mediados de los años 80 por el danés Bjarne Stroustrup. Su intención fue la de extender el lenguaje de programación C para que tuviese los mecanismos necesarios para manipular objetos.
Concurrencia en el lenguaje: C++ contiene los paradigmas de la programación estructurada y orientada a objetos. Se puede usar una librería thread que es básicamente una sección de código independiente que el procesador puede ejecutar de forma concurrente junto a otros threads o hilos de ejecución.
Aplicaciones notables: Motores gráficos como Source de Valve, usado en videojuegos como Half-Life y Counter-Strike.
Ejemplos Lenguages
- Ejemplo en python: Este es un pequeño ejemplo con el fin de ilustrar como podemos lanzar un hilo en python
from threading import Thread
import time
def say_hello(name):
print (name, "Hola")
t = Thread(target=say_hello, args=("world",))
t.start()
t.join()
Ejecucion: python archivo.py
('world', 'Hola')
- Ejemplo en GO: En este ejemplo podemos visualizar que los hilos no se ejecutan en el orden ascedente si no esto lo decide el scheduler del sistema operativo
package main
import (
"fmt"
"time"
)
const FINAL = 100 * time.Millisecond
func saluda(i int) {
time.Sleep(10 * time.Duration(i%5) * time.Millisecond )
fmt.Println("Hola a todos", i)
}
func main() {
for i := 1; i <= 6; i++ {
// Lanzamos nuestro hilo solo anteponiendo la palabra go a la funcion
go saluda(i)
}
time.Sleep(FINAL)
}
Ejecucion: go run archivo.go
Hola a todos 5
Hola a todos 6
Hola a todos 1
Hola a todos 2
Hola a todos 3
Hola a todos 4
- Ejemplos semaforo en C: El siguiente codigo fuente solo sirve sistemas operativos basado en unix.
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#define NUMHILOS 5
sem_t *semaforo;
int error, i, parametro, a;
char t;
void *z;
void *fun(void *ap )
{
sem_t *sem = ap;
char t;
sem_wait( sem ); // Bloqueamos seccion critica
printf( "\n Hilo ENTRO a seccion critica" );
fflush( stdout );
sleep( 1 );
printf( "\n Hilo SALIO a seccion critica\n\n" );
fflush( stdout );
sem_post( sem ); // Desbloqueamos seccion critica
}
int main()
{
// Creamos nuestro semaforo
semaforo = sem_open( "sema", O_CREAT, 0666, 1 );
// Declaramos el descritor de los hilos
pthread_t hilos[NUMHILOS];
// Creamos nuestros hilos
for( i = 0; i < NUMHILOS; i++ )
pthread_create( &hilos[i], NULL, (void *)fun, semaforo );
// Esperamos a que nuestro hilos de ejecutan antes de terminar el programa
for( i = 0; i < NUMHILOS; i++ )
pthread_join( hilos[i], (void *)&z );
// Destruimos nuestro semaforo
sem_unlink( "sema" );
sem_close( semaforo );
return 0;
}
Compilamos: gcc semaforo.c -o semaforo -pthread
Ejecutamos: ./semaforo
Hilo ENTRO a sección critica
Hilo SALIO a sección critica
Hilo ENTRO a sección critica
Hilo SALIO a sección critica
Hilo ENTRO a sección critica
Hilo SALIO a sección critica
- Ejemplo en C++: Calculamos el numero pi con 4 hilos con la ayuda de Serie de Leibniz
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
vector<double> valorCal( 4, 0.0 ), limites;
void calcularIntervalo (int index)
{
for (int i = limites[index]; i < limites[index+1]; i++)
if (i % 2 == 0)
valorCal[index] += 1.0/(2*i+1);
else
valorCal[index] -= 1.0/(2*i+1);
}
int main()
{
// Creamos nuestro limites
int numeroSerie = 1000000000;
limites.push_back( 0 );
limites.push_back( numeroSerie/4 );
limites.push_back( numeroSerie/2 );
limites.push_back( 3*(numeroSerie/4) );
limites.push_back( numeroSerie );
// Creamos a hilos
thread hilos[4];
// Inicializamos nuestros hilos
for (int i = 0; i < 4; i++)
hilos[i] = thread( calcularIntervalo, i );
// Esperemos a que nuestros hilos terminen
for (int i = 0; i < 4; i++)
hilos[i].join();
// Calculamos nuestra respuesta
double answer = 0;
for (int i = 0; i < 4; i++)
answer += valorCal[i];
answer *= 4;
printf( "EL valor de pi es:\t%.20f\n", answer );
return 0;
}
Compilar: g++ pi.cpp -o pi -std=c++11 -pthread
Ejecutar: ./pi
EL valor de pi es: 3.14159265258921038821
- Ejemplo en Java:
Un grupo de personas trata de ir de una isla(Oahu) a la otra(Molokai) con un solo bote.
Reglas:
- Cada persona es un hilo.
- La persona puede ser un adulto o un niño.
- Pueden ir dos niños en el bote o solo un adulto.
- El bote necesita como mínimo un piloto./li>
- las personas solo se pueden comunicar con los que estén en la misma isla.
- Siempre hay mínimo dos niños.
El principal objetivo del ejemplo es mostrar la sincronización de los hilos, la solución al problema consiste en llevar dos niños a Molokai, hacer que uno se devuelva con el bote, subir un adulto en oahu, cuando este llegue a Molokai se devolverá el otro niño que estaba ahí, y en Oahu se volverán a subir dos niños, este proceso se repetirá hasta que todas las personas estén en Oahu, hay que tener en cuenta que no hay algo como unidad principal que controle cuando pasará cada persona, cada hilo deberá saber cuando ejecutar sus instrucciones dependiendo de la sincronización con los otros
import java.util.concurrent.Semaphore; //extiende de thread y sobreescribe run sin nada public class Persona extends Thread{ int Tamano; String ubicacion; static String boatUbication="Oahu"; public static String getBoatUbication() { return boatUbication; } public static void setBoatUbication(String boatUbication) { Persona.boatUbication = boatUbication; } public Persona(int tamano, String ubicacion) { super(); this.Tamano = tamano; this.ubicacion = ubicacion; } @Override public void run() { } } import java.util.logging.Level; import java.util.logging.Logger; //extiende de persona public class Adult extends Persona { public Adult() { super(2, "Oahu"); } //sobreescribe run @Override public void run() { //mientras no hayan pasado todas las personas while(Boat.ContMolokai < Boat.ContTotalPersonas){ try { //semaforo encargado de que un niño y adulto no luchen por el bote Boat.classExclus.acquire(); if (this.ubicacion.equals("Oahu") && boatUbication.equals(this.ubicacion) && Boat.ContChildBack%2!=0 && Boat.ContAdultOahu != 0){ //si ve que puede pasar lo hace, adquiere las dos posiciones del bote Boat.available.acquire(); Boat.available.acquire(); Boat.bg.AdultRowToMolokai(); this.ubicacion="Molokai"; boatUbication="Molokai"; Boat.ContMolokai++; Boat.ContOahu--; Boat.ContAdultOahu--; //al llegar libera las dos posiciones del bote Boat.available.release(); Boat.available.release(); } Boat.classExclus.release(); } catch (InterruptedException ex) { Logger.getLogger(Child.class.getName()).log(Level.SEVERE, null, ex); } } } } import java.util.concurrent.Semaphore; import java.util.logging.Level; import java.util.logging.Logger; //parecida a Adult public class Child extends Persona { public Child() { super(1, "Oahu"); } @Override public void run() { while(Boat.ContMolokai < Boat.ContTotalPersonas ){ try { Boat.classExclus.acquire(); if (this.ubicacion.equals("Oahu") && boatUbication.equals(this.ubicacion) && (Boat.ContChildBack%2==0 || Boat.ContAdultOahu == 0) ){ //a diferencia del adulto el niño solo adquiere una posicion del bote Boat.available.acquire(); Boat.UbicacionDispon--; if (Boat.UbicacionDispon==1) Boat.bg.ChildRowToMolokai(); this.ubicacion="Molokai"; Boat.ContMolokai++; Boat.ContOahu--; if(Boat.UbicacionDispon==0){ Boat.bg.ChildRideToMolokai(); boatUbication="Molokai"; Boat.UbicacionDispon=2; } Boat.available.release(); } //parte de la seccion critica encargada de que el niño regrese desde Molokai a Oahu if (this.ubicacion.equals("Molokai") && boatUbication.equals(this.ubicacion) && (Boat.ContMolokai < Boat.ContTotalPersonas ) ){ //para evitar que se regresen dos niños el niño adquiere las dos posiciones //del bote Boat.available.acquire(); Boat.available.acquire(); Boat.bg.ChildRowToOahu(); this.ubicacion="Oahu"; boatUbication="Oahu"; Boat.ContMolokai--; Boat.ContOahu++; Boat.ContChildBack++; //llega y libera las dos posiciones Boat.available.release(); Boat.available.release(); } Boat.classExclus.release(); } catch (InterruptedException ex) { Logger.getLogger(Child.class.getName()).log(Level.SEVERE, null, ex); } } } } import java.util.Scanner; import java.util.concurrent.Semaphore; import java.util.LinkedList; public class Boat { //se instancian los dos semaforos public static Semaphore available = new Semaphore(2, true); public static Semaphore classExclus = new Semaphore(1, true); static Scanner sc = new Scanner(System.in); //contadores para la comunicacion static int childrenTot=0; static int adultTot=0; static int ContChilds=0; static int ContAdults=0; static int ContTotalPersonas=0; static int ContOahu=0; static int ContMolokai=0; static int ContAdultOahu=0; static int UbicacionDispon=2; static int ContChildBack=0; //arreglo de personas static LinkedListChildList = new LinkedList (); static LinkedList AdultList = new LinkedList (); static BoatGrader bg= new BoatGrader(); //se crean los hilos static public void begin(){ for(Child element : ChildList){ element.start(); } for(Adult element : AdultList){ element.start(); } //al terminar los hilos hacen join con el hilo principal for(Child element : ChildList){ try { element.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for(Adult element : AdultList){ try { element.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String args[]) { //entradas del programa while (childrenTot< 2){ System.out.println("Introduzca el numero de niños (mayor a 1): "); childrenTot=sc.nextInt(); } System.out.println("Introduzca el numero de adultos: "); adultTot=sc.nextInt(); for (int i=0;i< childrenTot;i++){ ChildList.add(new Child()); ContChilds++; } for (int i=0;i< adultTot;i++){ AdultList.add(new Adult()); ContAdults++; } ContTotalPersonas=ContChilds+ContAdults; ContAdultOahu=ContAdults; ContOahu=ContTotalPersonas; Boat.begin(); System.out.println("llegaron "+ContMolokai+" personas"); } }
ejecución:
Introduzca el numero de niños (mayor a 1): 3 Introduzca el numero de adultos: 5 **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Adult rowing to Molokai. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Adult rowing to Molokai. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Adult rowing to Molokai. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Adult rowing to Molokai. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Adult rowing to Molokai. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. **Child rowing to Oahu. **Child rowing to Molokai. **Child arrived on Molokai as a passenger. llegaron 8 personas
- Ejemplo en Go:
package main import( "fmt" "time" ) func main() { fmt.Println("iniciando") go printForward() go printBackwards() time.Sleep(time.Second * 5) fmt.Println("terminando") } func printForward() { for i := 0; i < 10; i++ { fmt.Println(i) time.Sleep(time.Millisecond) } } func printBackwards() { for i := 10; i <= 20; i++ { fmt.Println(i) time.Sleep(time.Millisecond) } }
En Go tenemos rutinas que tienen su propio stack, aunque estos no son hilos como tal, pero si se pueden aprovechar de esta forma
package main import( "fmt" "time" ) func main() { c := make(chan string) go echo(c) c < - "Hola" mensaje := < - c fmt.Println(mensaje) } func echo(c chan string) { msg := < - c time.Sleep(time.Second * 1) c < - fmt.Sprintf("Mensaje recibido: %s", msg) } salida= Mensaje recibido:Hola
En Go tenemos el Select es como un switch, pero que espera mensajes en canales. Su finalidad es comunicar, no comparar valores.
package main import( "fmt" "time" ) func main() { process1 := processExpensiveTransaction() process2 := processExpensiveTransaction() for i := 0; i < 2; i++ { select { case msg1 := <- process1: fmt.Println("Proceso 1 termino con status ", msg1) case msg2 := <- process2: fmt.Println("Proceso 2 termino con status ", msg2) } } } func processExpensiveTransaction() chan string { c := make(chan string) go func() { time.Sleep(time.Duration(rand.Intn(6)) * time.Second) c <- "ok" } () return c }
- Otro Ejemplo en Go:
En este ejemplo vamos a optimizar una aplicación que realiza peticiones http, haciendo uso de concurrencia, el siguiente código es la aplicación implementada de manera secuencial:
package main import ( "fmt" "log" "net/http" "os" ) func sendRequest(url string){ res, err := http.Get(url) if err != nil{ panic(err) } fmt.Printf("Estado: %d URL: %s\n", res.StatusCode,url) } func main() { if len(os.Args) < 2 { log.Fatalln("Uso: go run main.go url1 url2 .. urln") } for _, url := range os.Args[1:] { sendRequest("https://" + url) } }
Para hacer uso del programa, usamos el siguiente comando:
go run main.go url1 url2 .. urlnDonde url1 a urln se reemplazan por urls a las cuales queremos hacer la petición Get. Los resultados de esto deberían ser los siguientes, en el caso de haber usado las urls de google, youtube y facebook:
Estado: 200 URL: https://facebook.com Estado: 200 URL: https://youtube.com Estado: 200 URL: https://google.com
Para medir el tiempo de ejecución se debe usar el siguiente comando:
Para Windows:
Measure-Command {go run main.go url1 url2 .. urln}Para Linux:
time go run main.go url1 url2 .. urlnEste tiempo de ejecución es importante para comparar el antes y el después de usar concurrencia. Haciendo uso del comando de Windows, obtuvimos como resultado:
Days : 0 Hours : 0 Minutes : 0 Seconds : 1 Milliseconds : 970 Ticks : 19702343 TotalDays : 2,28036377314815E-05 TotalHours : 0,000547287305555556 TotalMinutes : 0,0328372383333333 TotalSeconds : 1,9702343 TotalMilliseconds : 1970,2343
Actualmente nuestra aplicación funciona de manera síncrona, lo cual ralentiza su procesamiento, debido a que tiene que esperar que cada petición termine para poder realizar la siguiente. Ahora vamos a modificar este programa inicial para usar concurrencia y optimizar nuestra aplicación:
package main import ( "fmt" "log" "net/http" "os" ) func sendRequest(url string){ res, err := http.Get(url) if err != nil{ panic(err) } fmt.Printf("Estado: %d URL: %s\n", res.StatusCode,url) } func main() { if len(os.Args) < 2 { log.Fatalln("Uso: go run main.go url1 url2 .. urln") } for _, url := range os.Args[1:] { go sendRequest("https://" + url) } }
Con solo agregar la palabra go, hacemos que cada consulta se ejecute en goroutines separadas y si ejecutamos el comando de Benchmark anteriormente mencionado vamos a observar que el tiempo de ejecucion es mucho menor:
Days : 0 Hours : 0 Minutes : 0 Seconds : 0 Milliseconds : 832 Ticks : 8328442 TotalDays : 9,63940046296296E-06 TotalHours : 0,000231345611111111 TotalMinutes : 0,0138807366666667 TotalSeconds : 0,8328442 TotalMilliseconds : 832,8442
Pero nos vamos a encontrar con un problema: no se imprimen los resultados de las goroutines en consola. Para corregir esto vamos a hacer uso de WaitGroup, el cual ayuda a contabilizar las rutinas que se tienen activas:
package main import ( "fmt" "log" "net/http" "os" ) var wg sync.WaitGroup func sendRequest(url string){ defer wg.Done() //Decrementa el contador de goroutines res, err := http.Get(url) if err != nil{ panic(err) } fmt.Printf("Estado: %d URL: %s\n", res.StatusCode,url) } func main() { if len(os.Args) < 2 { log.Fatalln("Uso: go run main.go url1 url2 .. urln") } for _, url := range os.Args[1:] { go sendRequest("https://" + url) wg.Add(1) //Incrementa el contador de goroutines } wg.Wait() //Indica que toca esperar a que todas las goroutines terminen }
Si ejecutamos de nuevo nuestro programa, vamos a ver que los resultados ahora si se muestran en consola.
Estado: 200 URL: https://facebook.com Estado: 200 URL: https://youtube.com Estado: 200 URL: https://google.com
Es posible que las goroutines se mezclen en consola, debido a que pueden terminar al mismo tiempo. Dando como resultado:
Estado: 200 URL: Estado: 200 URL: https://youtube.comhttps://facebook.com Estado: 200 URL: https://google.com
Para evitar que esto ocurra vamos a hacer uso de Mutex, el cual ayuda a sincronizar el acceso a recursos compartidos, como es el caso de la consola. Para hacer uso de Mutex, hacemos lo siguiente:
package main import ( "fmt" "log" "net/http" "os" ) var wg sync.WaitGroup var mut sync.Mutex func sendRequest(url string){ defer wg.Done() //Decrementa el contador de goroutines res, err := http.Get(url) if err != nil{ panic(err) } mut.Lock() //Bloquea el recurso inferior para que solo esta gorutine //pueda hacer uso de este defer mut.Unlock() //Desbloquea el recursos despues de haber terminado de usarlo fmt.Printf("Estado: %d URL: %s\n", res.StatusCode,url) } func main() { if len(os.Args) < 2 { log.Fatalln("Uso: go run main.go url1 url2 .. urln") } for _, url := range os.Args[1:] { go sendRequest("https://" + url) wg.Add(1) //Incrementa el contador de goroutines } wg.Wait() //Indica que toca esperar a que todas las goroutines terminen }
- Ejemplo de concurrecia en Erlang:
La función spawn en Erlang nos permiete crear un proceso en paralelo.
-module(helloworld). -export([start/0]). start() -> Pid = spawn(fun() -> server("Hello") end), server(Message) -> io:fwrite("~p",[Message]).
La salida es la siguiente:
"Hello"
El operador ! nos permite enviar mensajes a los procesos.
-module(helloworld). -export([start/0]). start() -> Pid = spawn(fun() -> server("Hello") end), Pid ! {hello}. server(Message) -> io:fwrite("~p",[Message]).
La salida es la siguiente:
"Hello"
receive permite recibir mensajes que son enviados a los procesos.
-module(helloworld). -export([loop/0,start/0]). loop() -> receive {rectangle, Width, Ht} -> io:fwrite("Area of rectangle is ~p~n" ,[Width * Ht]), loop(); {circle, R} -> io:fwrite("Area of circle is ~p~n" , [3.14159 * R * R]), loop(); Other -> io:fwrite("Unknown"), loop() end. start() -> Pid = spawn(fun() -> loop() end), Pid ! {rectangle, 6, 10}, Pid ! {circle, 6}, Pid ! {square, 4, 4}.
La salida es la siguiente:
Area of rectangle is 60
Area of circle is 113.09723999999999
Unknown
- Ejemplo de concurrecia en Rust:
Rust facilita la programación concurrente con las comprobaciones que se hacen en tiempo de compilación y con la gestión de memoria que realiza.
Hilos: creamos hilos con el comando thread::spawn, que recibe un closure y se lanza justo al definirlo.
use std::thread; fn main() { let child = thread::spawn(|| { println!("Hello from a thread!"); }); let _ = child.join(); }
La salida es la siguiente:
Hello from a thread!
Para esperar a que un hilo termine se puede utilizar el método join.
Podemos lanzar varios hilos dentro de un bucle
Aquí, en cada vuelta del bucle, iniciaremos un hilo con un iterador asociado. El primer hilo se inicia con el número 0, el segundo con el número 1 y así sucesivamente. Podemos ver en el resultado lo que se dijo antes: los subprocesos se lanzaron en un orden específico y, sin embargo, al mostrar los resultados, vemos que está completamente desordenado: de hecho, la velocidad de la ejecución de cada uno de los hilos variará. Si reiniciamos este programa, su salida y la visualización de los números serán diferentes; es imposible de predecir el orden de los números que aparecerán. Pero, si utilizamos el sistema de bloqueo y desbloqueo, para que cada hilo espere hasta que el anterior termine, el orden será perfecto.
use std::thread; fn main() { let mut childs = vec![]; for i in 0..10 { let child = thread::spawn(move || { println!("Hello from a thread! {}", i); }); childs.push(child); } for c in childs { let _ = c.join(); } }
La salida es la siguiente:
Hello from a thread! 1
Hello from a thread! 0
Hello from a thread! 5
Hello from a thread! 3
Hello from a thread! 7
Hello from a thread! 4
Hello from a thread! 8
Hello from a thread! 6
Hello from a thread! 2
Hello from a thread! 9
Mutex y Arc: Para compartir una referencia a memoria entre hilos se usa Arc y Mutex en combinación. Arc es un contador de referencias que se puede compartir entre hilos. Mutex implementa el bloqueo asociado a la variable en concreto.
use std::thread; use std::sync::{Arc, Mutex}; fn main() { let mut childs = vec![]; let shared = Arc::new(Mutex::new(String::from(""))); for i in 0..10 { let s = shared.clone(); let child = thread::spawn(move || { println!("In thread {}", i); let out = String::from("Thread ") + &i.to_string() + "\n"; s.lock().unwrap().push_str(&out); }); childs.push(child); } for c in childs { let _ = c.join(); } println!("\nOutput:\n{}", *(shared.lock().unwrap())); }
La salida es la siguiente:
In thread 0
In thread 3
In thread 5
In thread 4
In thread 2
In thread 6
In thread 7
In thread 8
In thread 1
In thread 9
Output:
Thread 0
Thread 3
Thread 5
Thread 4
Thread 2
Thread 6
Thread 7
Thread 8
Thread 1
Thread 9
En este ejemplo se define la cadena dentro de un Mutex y este dentro de un Arc, así se comparte la memoria
entre hilos.
channel crea un transmisor, tx, y un receptor, rx, en cada hilo, clona el transmisor y escribe en este la
salida. En el send se puede enviar cualquier tipo de dato según se cree el channel, no se pueden enviar
diferentes tipos de datos por el mismo canal.
use std::thread; use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); let mut childs = vec![]; for i in 0..10 { let tx = tx.clone(); let child = thread::spawn(move || { println!("In thread {}", i); let out = String::from("Thread ") + &i.to_string(); tx.send(out).unwrap(); }); childs.push(child); } for c in childs { let _ = c.join(); } println!("\nOutput:"); loop { match rx.try_recv() { Ok(x) => println!("{}", x), Err(_) => break } } }
La salida es la siguiente:
In thread 0
In thread 2
In thread 1
In thread 4
In thread 5
In thread 6
In thread 3
In thread 8
In thread 7
In thread 9
Output:
Thread 0
Thread 2
Thread 1
Thread 4
Thread 5
Thread 6
Thread 8
Thread 7
Thread 9
Thread 3
- Ejemplo de concurrencia en Crystal:
Crystal usa hilos llamados fibras para lograr concurrencia. Las fibras se comunican entre sí mediante
canales, como en Go o Clojure, sin tener que recurrir a la memoria compartida o bloqueos.
Cuando se inicia un programa, se activa la fibra principal que ejecutará su código de nivel superior.
Allí, uno puede engendrar (spawn) más fibras.
Los componentes de un programa son:
- Runtime Scheduler, a cargo de ejecutar todas las fibras cuando sea el momento adecuado.
- El bucle de eventos, que es solo otra fibra, está a cargo de tareas asíncronas, como por ejemplo archivos, sockets, pipes, señales y temporizadores.
- Canales, para comunicar datos entre fibras. Runtime Scheduler coordinará fibras y canales para su comunicación.
- Garbage collector: para limpiar la memoria que "ya no se usa".
spawn do loop do puts "Hello!" end end sleep 1.second
Este programa imprimirá "¡Hello!" por un segundo y luego saldrá. Esto se debe a que la llamada de espera
programará la fibra principal que se ejecutará en un segundo y luego ejecutará otra fibra "lista para
ejecutarse", que en este caso es la de arriba.
Otra manera seria:
spawn do loop do puts "Hello!" end end Fiber.yield
Fiber.yield le dirá al scheduler que ejecute la otra fibra. Esto imprimirá "Hello" hasta los bloques de salida estándar y luego la ejecución continuará con la fibra principal y el programa saldrá.
Creando(spawn) una llamada:
El programa imprime los números del 0 al 9. Se crea un Proc y se invoca pasando I, por lo que el valor se
copia y la fibra engendrada recibe una copia.
i = 0 while i < 10 proc = ->(x : Int32) do spawn do puts(x) end end proc.call(i) i += 1 end Fiber.yield
La salida es la siguiente:
0
1
2
3
4
5
6
7
8
9
Canales:
Cuando el programa ejecuta una recepción, esa fibra se bloquea y la ejecución continúa con la otra fibra.
Cuando se ejecuta un envió, la ejecución continúa con la fibra que estaba esperando en ese canal.
channel = Channel(Int32).new spawn do puts "Before first send" channel.send(1) puts "Before second send" channel.send(2) end puts "Before first receive" value = channel.receive puts value # => 1 puts "Before second receive" value = channel.receive puts value # => 2
La salida es la siguiente:
Before first receive
Before first send
1
Before second receive
Before second send
2
- Ejemplo en C con el juego de pacman:
Aquí, el programa debe poder recibir la entrada teclado del usuario. Pero también, al mismo tiempo, tienes que:
- Calcula los movimientos de los enemigos gracias a la inteligencia artificial.
- Ver los diferentes desplazamientos.
- Actualizar el mapa
int main_loop(IGame *Game, IGui *curse):
{
board game;
t_orientation dir;
int k = 0;
while (k == 0)
{
dir = curse->get_touch();
if (dir > 4)
return (dir);
k = Game->check_move(dir);
game = Game->get_board();
if (curse->display(game) == -1)
return (0);
}
delete curse;
delete Game;
return (0);
}
Si observamos el código que se muestra, la función get_touch iniciará un hilo que se ocupará de recuperar la entrada del teclado pero no bloqueará el resto del programa: no esperará a recuperar una tecla para continuar.
Ejemplo Comparativo
Suma concurrente
Dado un arreglo de números, queremos sumar todos sus elementos. Utilizaremos programación concurrente para dividir el trabajo entre varios workers y sumar los resultados parciales. Esta solución será implementada en tres lenguajes diferentes: Go, Elixir y Rust. Cada lenguaje tiene sus propias características y formas de manejar la concurrencia. A continuación, se presenta el código para cada uno de estos lenguajes seguido de una tabla comparativa que detalla las diferentes secciones del código y cómo se implementan en cada lenguaje.
Go
package main
import (
"fmt"
"sync"
)
func worker(arr []int, wg *sync.WaitGroup, resultChan chan int) {
defer wg.Done()
sum := 0
for _, num := range arr {
sum += num
}
resultChan <- sum
}
func main() {
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
numWorkers := 2
chunkSize := len(arr) / numWorkers
resultChan := make(chan int, numWorkers)
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
start := i * chunkSize
end := start + chunkSize
if i == numWorkers-1 {
end = len(arr)
}
wg.Add(1)
go worker(arr[start:end], &wg, resultChan)
}
wg.Wait()
close(resultChan)
totalSum := 0
for sum := range resultChan {
totalSum += sum
}
fmt.Printf("Total Sum: %d\n", totalSum)
}
Elixir
defmodule ConcurrentSum do def worker(pid, list) do sum = Enum.sum(list) send(pid, {self(), sum}) end def start(list) do num_workers = 2 chunk_size = div(length(list), num_workers) pids = for i <- 1..num_workers do start = (i - 1) * chunk_size + 1 finish = min(start + chunk_size - 1, length(list)) spawn(ConcurrentSum, :worker, [self(), Enum.slice(list, start - 1, finish - start + 1)]) end receive_results(pids, 0) end defp receive_results([], acc) do IO.puts("Total Sum: #{acc}") end defp receive_results(pids, acc) do receive do {_, sum} -> receive_results(tl(pids), acc + sum) end end end ConcurrentSum.start([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
Rust
use std::sync::mpsc; use std::thread; fn worker(arr: &[i32], tx: mpsc::Sender) { let sum: i32 = arr.iter().sum(); tx.send(sum).expect("Could not send data!"); } fn main() { let arr = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let num_workers = 2; let chunk_size = arr.len() / num_workers; let (tx, rx) = mpsc::channel(); let mut handles = vec![]; for i in 0..num_workers { let start = i * chunk_size; let end = if i == num_workers - 1 { arr.len() } else { start + chunk_size }; let chunk = arr[start..end].to_vec(); let tx = tx.clone(); let handle = thread::spawn(move || { worker(&chunk, tx); }); handles.push(handle); } for handle in handles { handle.join().expect("Thread couldn't join!"); } let total_sum: i32 = rx.iter().take(num_workers).sum(); println!("Total Sum: {}", total_sum); }
Tabla Comparativa
Sección | Go | Elixir | Rust |
---|---|---|---|
División del Trabajo |
for i := 0; i < numWorkers; i++ { start := i * chunkSize end := start + chunkSize if i == numWorkers-1 { end = len(arr) } wg.Add(1) go worker(arr[start:end], &wg, resultChan) } |
pids = for i <- 1..num_workers do start = (i - 1) * chunk_size + 1 finish = min(start + chunk_size - 1, length(list)) spawn(ConcurrentSum, :worker, [self(), Enum.slice(list, start - 1, finish - start + 1)]) end |
for i in 0..num_workers { let start = i * chunk_size; let end = if i == num_workers - 1 { arr.len() } else { start + chunk_size }; let chunk = arr[start..end].to_vec(); let result = Arc::clone(&result); let handle = thread::spawn(move || { let sum: i32 = chunk.iter().sum(); let mut total = result.lock().unwrap(); *total += sum; }); handles.push(handle); } |
Sincronización |
var wg sync.WaitGroup wg.Wait() close(resultChan) |
send(pid, {self(), sum}) |
let result = Arc::new(Mutex::new(0)); |
Recolección de Resultados |
totalSum := 0 for sum := range resultChan { totalSum += sum } fmt.Printf("Total Sum: %d\n", totalSum) |
receive do {_, sum} -> receive_results(tl(pids), acc + sum) end |
for handle in handles { handle.join().unwrap(); } println!("Total Sum: {}", *result.lock().unwrap()); |