Concurrencia

El concepto de concurrencia fue considerado en las computadoras cuando los procesadores obtuvieron la capacidad de multiprogramación. La multiprogramación es una técnica de los sistemas operativos que permite que múltiples programas residan en la memoria principal y sean ejecutados por una única CPU de manera concurrente, alternándose en su uso a través de cambios de contexto.

En la actualidad la concurrencia es tremendamente vigente y debe ser considerada para elaborar programas distribuidos, tolerantes a fallos, escalables y en tiempo real. El hardware actual tiene capacidades de paralelismo, la industria de procesadores añade más núcleos cada vez. Aumentando la velocidad de procesamiento cada año y las tecnologías, sobre todo los lenguajes de programación deben ser capaces de solucionar problemas de concurrencia, o se arriesgan a quedar obsoletos. La concurrencia es un problema que aparece en multitud de casos fuera de los sistemas operativos, como sistemas multi usuarios, sistemas de control de telecomunicaciones y en general sistemas que evolucionan en el tiempo.

La programación con concurrencia es difícil. La concurrencia puede hacer que los programas sean más rápidos que los secuenciales, pero tener múltiples hilos que leen y actualizan variables compartidas simultáneamente y se sincronizan entre sí hace que los programas sean más complicados que aquellos donde solo ocurre una cosa a la vez.

¿Por qué los programas concurrentes son más complicados que los secuenciales? Hay, al menos, dos razones:

  • La ejecución de un programa secuencial suele ser determinista: si ejecutas el programa dos veces con la misma entrada, se producirá la misma salida. Los errores son reproducibles y, por lo tanto, fáciles de detectar, por ejemplo, mediante la instrumentación del programa. Estos se llaman Bohrbugs[1]. Sin embargo, la salida de ejecutar programas concurrentes depende de cómo se entrelacen las ejecuciones de los diferentes hilos. Algunos errores pueden ocurrir solo ocasionalmente y pueden no ocurrir nunca cuando el programa está instrumentado para encontrarlos. A estos los llamamos Heisenbugs[2]: la sobrecarga causada por la instrumentación provoca cambios en el tiempo que pueden hacer que estos errores sean menos propensos a causar problemas.

  • En un programa secuencial, cada instrucción y cada función pueden considerarse atómicas (indivisibles) porque no hay otra actividad que interfiera con su ejecución. Aunque una instrucción o función pueda compilarse en múltiples instrucciones de máquina, se ejecutan consecutivamente hasta completarse. No ocurre así con un programa concurrente, donde otros hilos pueden actualizar ubicaciones de memoria mientras una instrucción o función se está ejecutando.

La falta de determinismo y atomicidad en los programas concurrentes hace que no solo sean difíciles de entender, sino también difíciles de probar. Ejecutar la misma prueba de código concurrente dos veces probablemente producirá dos resultados diferentes. Más problemático aún, una prueba puede activar un error solo en ciertas ejecuciones "afortunadas". Debido a la naturaleza probabilística del código concurrente, algunos errores pueden ser muy poco probables de activarse incluso al ejecutar una prueba millones de veces. Y, aun si se activa un error, la fuente del mismo puede ser difícil de encontrar porque la ejecución incorrecta es difícil de reproducir.

¿Qué es el determinismo y la atomicidad?

Para profundizar un poco en lo que es determinismo y atomicidad debemos recurrir a las matemáticas discretas, en específico a la lógica proposicional.

  • Una proposición es una secuencia de afirmaciones, la última afirmación se llama conclusión y el resto se llama premisas.

  • Una proposición es válida siempre y cuando la conclusión sea verdad y todas sus premisas sean verdaderas.

  • Una proposición es inválida si su conclusión o alguna de sus premisas son falsas. Incluso esto puede pasar si todas sus premisas son verdad y solamente su conclusión es falsa.

  • Una proposición es sólida y válida si todas sus premisas incluyendo la conclusión son verdaderas.

  • Si los componentes de una proposición no pueden ser determinados en verdadero o falso, entonces no se puede deteminar la validez de la proposición. Ya que la validez describe la relación entre las premisas y la conclusión de una proposición.

  • Una prueba de una afirmación es una proposición sólida cuya conclusión es la afirmación.

  • Una afirmación es una frase declarativa que puede ser verdadera o falsa.

  • Una afirmación puede ser molecular (puede subdividirse en más afirmaciones) o atómica (no puede subdividirse).

Ejercicio 1: Los trolls del bosque

Estas en una aventura por un bosque encantado y encuentras tres trolls que bloquean el paso por un puente. Cada uno es un caballero (solo dice la verdad) o un bribón (solo dice mentiras). Los trolls no te dejarán pasar a menos que determines correctamente quien es caballero y quien es bribón. Para lo cual te dicen las siguientes proposiciones:

  • Troll 1: Si soy un bribón, entonces hay exactamente dos caballeros.

  • Troll 2: El primer troll está mintiendo.

  • Troll 3: O todos somos bribones o al menos uno es un caballero.

Preguntas

  • ¿Qué troll es caballero o bribón?[3]

  • ¿Qué ocurre si sabemos que el primer troll es un bribón?.[4]

Ejercicio 2: El postre

Proposición 1

  • Si Eliana come todas sus verduras, ella podrá comer postre.

  • Eliana comió todas sus verduras

  • Por lo tanto Eliana puede comer postre.

Proposición 2

  • Ernestina debe comer todas sus verduras para poder comer postre.

  • Ernestina comió todas sus verduras.

  • Por lo tanto Ernestina puede comer postre.

Preguntas

  • ¿Cuál proposición es válida y cuál es inválida?, ¿Por qué?.[5]

Ejercicio 3: ¿Afirmación o no?

Deteminar si son afirmaciones o no:

  • La luna esta hecha de queso.

  • .

  • ¿Te gustaría una sopaipilla?.

  • La suma de dos cuadrados.

  • ¡Haz la tarea!.

  • [6]

¿Qué es la concurrencia?

En el mundo se puede ver distintas actividades sucediendo al mismo tiempo. Por ejemplo en una plaza puede haber personas caminando, perros ladrando, aves volando y cantando, autos tocando su bocina, entre muchas cosas. Todas estas actividades suceden en paralelo. En el mundo real las palabras como simultáneo, paralelo, concurrente podrían ser sinónimos, pero en informática se debe ser más preciso y se debe definir la diferencia entre paralelo y concurrente.

Si solamente tuvieramos un procesador de un solo núcleo, no se puede ejecutar programas en paralelo. Esto es por que la CPU solo puede hacer una tarea a la vez. Pero se puede ejecutar programas concurrentes en una computadora con procesador mono núcleo. El sistema operativo permite agendar porciones de tiempo en el procesador a diferentes tareas, manteniendo la ilusión de que distintas tareas se ejecutan en paralelo.

Tomemos por ejemplo el caso de la plaza con personas paseando perros. Cada persona y cada perro puede ser un hilo (thread) (tambien conocido como procesos ligeros en la antigüedad) que ejecutará tareas específicas. Las personas pueden caminar y los perros pueden ladrar.

El siguiente código de Erlang demuestra la creación de hilos con la función spawn, la cual devuelve un identificador (pid) que puede ser usado en el futuro para interactuar con el hilo. Este hilo es manejado por la máquina virtual de Erlang la cual crea el hilo y comienza a evaluar el código especificado por los argumentos. En este caso se llama al modulo person con la función init y el argumento "Juan".

-module(plaza).
-export([start/0]).

start() ->
  Juan = spawn(person, init, ["Juan"]),
  Cachupin = spawn(dog, init, ["Cachupin", Juan])

Por lo tanto la principal diferencia entre paralelismo y concurrencia es que cuando algo es ejecutado en paralelo, se tienen garantías de que hay varias tareas procesandose al mismo tiempo, en varios procesadores o núcleos por separado. Mientras que cuando hay algo concurrente, se puede ejecutar en un solo procesador, pero no hay garantías (determinismo) de que las tareas sean evaluadas en el mismo orden, al mismo tiempo y demoren lo mismo o den el mismo resultado para los mismos parámetros.

Puede parecer extraño requerir que los programadores asuman que el procesador virtual de un hilo funcione a una velocidad impredecible y que cualquier entrelazado (interleaving) con otros hilos suceda de manera no determinista.

El modelo de programación con hilos adopta esta suposición como una forma de guiar a los programadores al razonar sobre la corrección. En lugar de asumir que un hilo se ejecuta a la misma velocidad que otro (o más rápido o más lento) e intentar escribir programas que coordinen los hilos basándose en su velocidad relativa de ejecución, los programas multihilo (concurrentes) no deben hacer suposiciones sobre el comportamiento del planificador de hilos (Scheduler). A su vez, las decisiones del kernel para asignar un hilo a un procesador y para interrumpirlo en favor de otro hilo pueden tomarse sin preocuparse de si afectan la corrección del programa.

La variabilidad en la velocidad de ejecución también ocurre durante el funcionamiento normal. Acceder a la memoria puede detener el procesador durante cientos o miles de ciclos si ocurre un fallo de caché. Otros factores incluyen la frecuencia con la que el planificador interrumpe el hilo, la cantidad de procesadores físicos presentes en una máquina, el tamaño de las cachés, la velocidad de la memoria, cómo el firmware de ahorro de energía ajusta las velocidades de reloj de los procesadores, qué mensajes de red llegan o qué entrada se recibe del usuario.

Las velocidades de ejecución de los diferentes hilos de un programa son difíciles de predecir, pueden variar en hardware diferente e incluso pueden variar de una ejecución a otra en el mismo hardware.

Como resultado, debemos coordinar las acciones de los hilos mediante sincronización explícita en lugar de intentar razonar sobre su velocidad relativa.

Beneficios de la Concurrencia

La programación concurrente puede usarse para mejorar el rendimiento, crear sistemas escalables y tolerantes a fallos, y para escribir programas claros y comprensibles que controlen aplicaciones del mundo real. Permite aprovechar mejor los procesadores multinúcleo, ya que múltiples tareas pueden ejecutarse simultáneamente, aumentando la velocidad de ejecución.

También facilita la solución de problemas que son inherentemente concurrentes, como los sistemas de control con múltiples sensores y procesos actuando simultáneamente.

Además, la concurrencia mejora la utilización de recursos al evitar tiempos de inactividad mientras otras tareas esperan, contribuye a la capacidad de respuesta de las aplicaciones, y es fundamental para diseñar sistemas que deben manejar múltiples solicitudes o procesos a la vez. Por estas razones, la programación concurrente es crucial para el desarrollo de software moderno, especialmente en hardware con múltiples núcleos y en sistemas distribuidos.

Rendimiento

Imagina que tienes dos tareas: A, que tarda diez segundos en realizarse, y B, que tarda quince segundos. En una sola CPU haciendo ambas, A y B tardarán veinticinco segundos. En una computadora con dos CPUs que operan independientemente, hacer A y B tomará solo quince segundos. Para lograr esta mejora en el rendimiento, debemos escribir un programa concurrente.

Hasta hace poco, las computadoras paralelas eran raras y costosas, pero hoy las computadoras multicore son comunes. Un procesador de alta gama tiene sesenta y cuatro núcleos, y se espera que el número de núcleos por chip aumente de forma constante en el futuro previsible. Si tienes un problema adecuado y un hardware que lo permita, la concurrencia puede aprovechar estos múltiples núcleos para ejecutar tareas simultáneamente, mejorando significativamente el desempeño y reduciendo el tiempo total de ejecución en comparación con los programas secuenciales que usan un solo núcleo.

Esta mejora en el rendimiento es una de las principales razones para utilizar la programación concurrente, especialmente en el contexto del hardware moderno multicore, donde aprovechar al máximo los recursos disponibles es esencial para obtener eficiencia y velocidad.

Una computadora con sesenta y cuatro núcleos podría hacer que tu programa sea sesenta y cuatro veces más rápido al ejecutarse en ella, pero solo si escribes un programa concurrente.

Uno de los problemas más urgentes en la industria informática es la dificultad de paralelizar código secuencial legado para que pueda ejecutarse en una computadora multicore. Sin embargo, no existe tal problema en Erlang. Los programas Erlang escritos hace veinte años para una máquina secuencial simplemente se ejecutan más rápido cuando los corremos en multicore modernos.

Escalabilidad

Los programas concurrentes están formados por pequeños procesos independientes. Debido a esto, podemos escalar fácilmente el sistema aumentando el número de procesos y agregando más CPUs. En tiempo de ejecución, la máquina virtual de Erlang distribuye automáticamente la ejecución de procesos entre las CPUs disponibles.

Tolerancia a fallos

La tolerancia a fallos es similar a la escalabilidad. Las claves para la tolerancia a fallos son la independencia y la redundancia del hardware. Los programas en Erlang están compuestos por muchos pequeños procesos independientes. Los errores en un proceso no pueden causar accidentalmente que otro proceso falle. Para protegerse contra la falla de una computadora completa (o centro de datos), se necesita detectar fallas en computadoras remotas. Tanto la independencia de los procesos como la detección remota de fallos están integradas en la máquina virtual de Erlang.

Erlang fue diseñado para construir sistemas de telecomunicaciones tolerantes a fallos, pero la misma tecnología puede aplicarse igualmente para construir sistemas web escalables y tolerantes a fallos o servicios en la nube.

Claridad

En el mundo real las cosas suceden en paralelo, pero en la mayoría de los lenguajes de programación las cosas suceden de manera secuencial. La discrepancia entre el paralelismo del mundo real y la secuencialidad de nuestros lenguajes de programación hace que escribir problemas de control del mundo real en un lenguaje secuencial sea artificialmente difícil.

En Erlang, podemos mapear el paralelismo del mundo real sobre la concurrencia de Erlang de manera directa. Esto resulta en un código claro y fácil de entender.

Programas Concurrentes

Un programa concurrente es un programa escrito en un lenguaje de programación concurrente. Escribimos programas concurrentes por razones de rendimiento, escalabilidad o tolerancia a fallos.

Un lenguaje de programación concurrente es un lenguaje que tiene construcciones explícitas para escribir programas concurrentes. Estas construcciones son una parte integral del lenguaje y se comportan igual en todos los sistemas operativos.

Una computadora paralela es una computadora que tiene varias unidades de procesamiento (CPUs o núcleos) que funcionan al mismo tiempo.

Habiendo escrito un programa concurrente, podemos ejecutarlo en una computadora paralela. Podemos ejecutarlo en una computadora multicore, en un conjunto de computadoras en red o en la nube.

En una computadora multicore, el sistema operativo podría decidir apagar un núcleo para ahorrar energía. En la nube, una computación podría ser suspendida y trasladada a una nueva computadora. Estas son cosas fuera de nuestro control.

Hemos visto la diferencia entre un programa concurrente y una computadora paralela. La concurrencia tiene que ver con la estructura del software; el paralelismo tiene que ver con el hardware.


1. Niels Henrik David Bohr, fue un físico danés que contribuyó en la comprensión del átomo y la mecánica cuántica. La denominación incluye como parte inicial del nombre al átomo de Bohr porque representa un error sólido y fácilmente detectable.
2. Werner Karl Heisenberg , el físico que dedujo el efecto de observación de la mecánica cuántica, según el cual el mero hecho de observar un sistema de una manera determinada altera el estado de este.
3. Troll 1: Caballero, Troll 2: Bribón, Troll 3: Caballero
4. Troll 1: Bribón, Troll 2: Caballero, Troll 3: Caballero, Pero esto hace que la afirmación de Troll 1 sea verdadera y no puede ser por que el troll es bribón, por lo que este caso no es válido.
5. La primera proposición es válida, la segunda no lo es. Ya que no sabemos que otras condiciones tiene Ernestina para conseguir comer postre, por ejemplo que deba hacer su tarea o limpiar su cuarto. Lo que hace que la proposición no se pueda determinar por falta de información.
6. No es una afirmación por que tiene una variable, dependiendo del valor de la afirmación puede ser verdadera o falsa.