Mis on samaaegne programmeerimine ja kuidas seda omandada

Viimane uuendus: 28/02/2026
Autor: Isaac
  • La programación concurrente modela tareas que se solapan en el tiempo, diferenciándose del paralelismo físico pero pudiendo aprovecharlo.
  • Procesos, hilos, corrutinas, event loops y actores ofrecen distintos modelos para estructurar concurrencia, cada uno con sus ventajas.
  • Problemas como condiciones de carrera, deadlocks o inanición exigen mecanismos de sincronización y políticas de planificación cuidadosas.
  • Modelos formales como redes de Petri y CSP, junto con herramientas como FDR o JCSP, permiten verificar y simplificar sistemas concurrentes complejos.

programacion concurrente

La samaaegne programmeerimine se ha convertido en una pieza clave en el desarrollo de software moderno: sistemas operativos, servidores web, apps móviles, videojuegos, bases de datos… casi todo lo que usamos a diario ejecuta varias tareas a la vez, o al menos lo parece. Entender bien cómo funciona esta forma de programar, en qué se diferencia del paralelismo y qué problemas acarrea es fundamental si quieres escribir código robusto y eficiente.

Järgmistes ridades me analüüsime qué es exactamente la concurrencia, cómo se modela a nivel de procesos e hilos, qué modelos existen (hilos, corrutinas, actores, event loop…), qué errores típicos aparecen (condiciones de carrera, deadlocks, inanición, false sharing…) y qué herramientas tenemos para sincronizar y verificar sistemas concurrentes. Verás que, más allá de la teoría, todo esto tiene una traducción muy práctica en el diseño de software real.

Qué es la programación concurrente y cómo se diferencia del paralelismo

Cuando hablamos de concurrencia nos referimos a la capacidad de un sistema para gestionar múltiples tareas cuyo tiempo de vida se solapa, de modo que sus pasos de ejecución se entremezclan. La idea importante es el solapamiento lógico: desde el punto de vista del programador, hay varias actividades “vivas” a la vez, aunque físicamente un solo procesador solo pueda ejecutar una instrucción en cada instante.

La ejecución se organiza como un põimimine: el planificador (scheduler) va dando pequeños turnos de CPU a cada tarea. Hace un trozo de la tarea A, luego pasa a la B, después a la C, y más tarde vuelve a la A, siempre respetando las dependencias de datos. La programación concurrente, por tanto, es una forma de estructurar programas en varias actividades casi independientes, de manera que el orden exacto en el que se intercalan no está totalmente fijado.

El paralelismo es otra historia: aquí hablamos de que varias tareas se ejecutan literalmente al mismo tiempo en distintos núcleos o procesadores (gestionar los núcleos de la CPU). Un sistema puede ser concurrente y no paralelo (un solo núcleo que va cambiando de tarea) o concurrente y paralelo a la vez (varios núcleos ejecutando en paralelo algunos de esos hilos o procesos concurrentes).

En la práctica, la concurrencia busca modelar problemas donde ocurren múltiples cosas a la vez (peticiones web, clics del usuario, E/S de disco o red, sensores, etc.), mientras que el paralelismo se centra en acelerar cálculos pesados dividiéndolos en subproblemas que se ejecutan simultáneamente. Un mismo diseño concurrente puede ejecutarse de manera meramente intercalada o explotar paralelismo real si el hardware dispone de varios núcleos.

concurrencia y paralelismo

Procesos, hilos y estados de ejecución

En un sistema operativo, el concepto básico para soportar concurrencia es el protsess. Un proceso no es solo el archivo ejecutable en disco: es una entidad activa con su propio estado de ejecución. Incluye un programmiloendur (un registro de CPU que apunta a la próxima instrucción a ejecutar), una pila o stack del proceso (parámetros de funciones, direcciones de retorno, variables locales…), una sección de datos (variables globales, datos estáticos) y una zona de hunnik donde se aloja memoria dinámica en tiempo de ejecución.

El archivo binario almacenado en disco es solo un programa pasivo, una secuencia de instrucciones y datos. Para que ese programa “cobre vida” es necesario que el sistema operativo cree un proceso, inicialice su contexto (registros, memoria, descriptores de archivos, etc.) y lo someta a planificación para que reciba tiempo de CPU.

Estados de un proceso

Mientras un proceso existe, va transitando por distintos estados de ejecución, que suelen ser al menos los siguientes:

  • uus: el sistema está creando el proceso y configurando sus estructuras internas.
  • Jooksmine: el proceso tiene la CPU y sus instrucciones se están llevando a cabo.
  • Bloqueado o en espera: el proceso aguarda a que ocurra un evento (entrada/salida, fin de una operación, llegada de un mensaje…).
  • Listo o preparado: tiene todo lo necesario para ejecutarse, pero está esperando turno de CPU.
  • Valmis: ha finalizado su ejecución y el sistema liberará sus recursos.

El cambio entre estos estados lo decide el planeerija del sistema operativo según su política (round robin, prioridades, etc.), equilibrando el uso de CPU, la latencia y la justicia entre procesos.

Bloque de control de proceso (PCB)

Cada proceso se representa en el núcleo mediante una estructura de datos conocida como PCB (protsessi juhtimisplokk). Este bloque es el “expediente” del proceso y almacena toda la información necesaria para detenerlo y reanudarlo sin perder nada.

En el PCB se guarda el protsessi staatus (nuevo, listo, ejecutando, bloqueado…), el programmiloendur praegune, CPU registrid (cuyo número y tipo dependen de la arquitectura), la información de planificación (prioridad, punteros a las colas de ready, estadísticas de uso), y los datos de mälu haldamine (tablas de páginas, tablas de segmentos, etc.). Gracias a esto, el sistema operativo puede hacer konteksti muutused, es decir, guardar el estado completo de un proceso y restaurar el de otro.

Hilos y multihilo

Además de procesos, muchos sistemas operativos proporcionan hukkamise niidid (threads). Un hilo es una unidad básica de uso de CPU: tiene su propio identificador, contador de programa, registros y pila, pero comparte con otros hilos del mismo proceso la sección de código, la de datos y otros recursos como descriptores de archivos.

En un programa multihilo, varios hilos pueden ejecutar distintas partes del mismo código en paralelo o intercaladamente. Cada uno mantiene su propio stack (con sus variables locales y contexto de llamadas) pero todos ven el mismo espacio de direcciones del proceso. Esto facilita la comunicación (compartiendo memoria) a costa de introducir riesgos de concurrencia como las condiciones de carrera.

En un diseño multihilo típico, el proceso tiene un único PCB y un único espacio de direcciones, mientras que cada hilo añade su propia pila y su contexto de registros. El sistema operativo (o el runtime del lenguaje) implementa un modelo apropiativo: puede interrumpir un hilo en cualquier momento y dar la CPU a otro, lo que obliga a programar pensando en que tu hilo puede ser pausado entre dos instrucciones aparentemente “atómicas”.

Multiprogramación, multiproceso y procesamiento distribuido

Conviene distinguir varios niveles de ejecución concurrente. La multiprogramación permite que múltiples procesos residan en memoria y se turnen la CPU de un único procesador; nunca se ejecutan en paralelo de verdad, pero sí de forma intercalada.

El mitmeprotsessoriline (o multiproceso) utiliza dos o más procesadores o núcleos en una misma máquina para ejecutar uno o varios procesos, pudiendo lograr paralelismo real. Cada núcleo puede llevar un hilo distinto de forma simultánea, lo que multiplica el rendimiento en muchas cargas de trabajo (consulta las novedades clave de Intel y AMD sobre CPUs).

El hajutatud töötlemine va un paso más allá: uno o varios procesos se ejecutan en diferentes ordenadores comunicados por red. Aquí entran en juego problemas adicionales (latencias, fallos parciales, particiones de red), pero el modelo conceptual sigue siendo concurrente: muchas actividades en marcha que deben coordinarse.

Modelos de programación concurrente: hilos, corrutinas, event loop y actores

modelos de programacion concurrente

La concurrencia no se programa siempre igual. Existen varios modelos de programación concurrente que ofrecen distintas abstracciones y compensan de forma diferente facilidad de uso, rendimiento y paralelismo. Entre los más habituales están los hilos del sistema, las corrutinas, los bucles de eventos y el modelo de actores.

  Xamarin: kõik, mida peate selle raamistiku kohta teadma

Korutiinid

Una corrutina es una forma de concurrencia cooperativa dentro de un mismo hilo de sistema operativo. A diferencia de los hilos tradicionales, donde el scheduler del SO decide cuándo interrumpir y reanudar, las corrutinas ceden el control explícitamente cuando el programador lo indica (por ejemplo, al esperar E/S o al finalizar una parte de la tarea).

Seda mudelit nimetatakse ühistu porque cada corrutina debe estar diseñada para “portarse bien”: debe ceder la ejecución en puntos adecuados para no bloquear al resto. A cambio, el contexto de una corrutina es mucho más ligero que el de un hilo: muchas corrutinas pueden vivir dentro de un solo hilo de sistema, compartiendo stack o usando stacks muy pequeños.

Por defecto, las corrutinas modelan concurrencia sin paralelismo: todas se intercalan dentro de un único hilo. Sin embargo, muchos runtimes modernos permiten combinarlas con múltiples hilos para obtener paralelismo real si es necesario. Conceptos como promesas/futuros o async/await suelen integrarse de forma natural con este modelo.

Event loop o bucle de eventos

Otro patrón muy influyente es el del sündmuste tsükkel. En este enfoque, hay generalmente un único hilo que mantiene un ciclo central donde se atienden eventos (llegada de datos por red, timers, entradas de usuario…) y se ejecutan callbacks registrados previamente para reaccionar a cada tipo de evento.

Se habla de modelo bloqueante diferido porque, en lugar de bloquearse esperando E/S, las operaciones registran un manejador que se ejecutará cuando el evento ocurra al final de un ciclo del bucle. Esto permite describir múltiples actividades concurrentes en un solo hilo, evitando la complejidad de la sincronización entre hilos y dando un uso muy eficiente de la CPU.

De forma predeterminada, este modelo proporciona concurrencia pero no paralelismo (todo pasa por el mismo hilo), aunque muchas plataformas permiten escalar a varios hilos o procesos bajo el capó. Un aspecto clave es que la kohalik mälu y el ámbito de las variables se comportan de manera muy parecida a un programa secuencial, lo que reduce el riesgo de condiciones de carrera.

En el diseño de APIs basadas en event loop se utilizan con frecuencia clausuras o callbacks para capturar el entorno necesario. Esto puede derivar en el conocido problema de la “pirámide de la perdición”, donde la anidación de callbacks hace el código difícil de leer. Alternativas como promesas/futuros o las palabras clave async/await mejoran enormemente la ergonomía.

Modelo de actores

En el modelo de actores, la unidad básica de ejecución es el näitleja, una entidad que encapsula su propio estado y comportamiento y se comunica exclusivamente mediante el envío de mensajes. Cada actor dispone de una cola de mensajes y procesa esos mensajes de forma secuencial, uno por uno, lo que evita compartir memoria mutable directamente.

Los actores son, conceptualmente, procesos extremadamente ligeros gestionados por un runtime o máquina virtual, no directamente por el sistema operativo. Esto permite crear miles o incluso millones de actores dentro de un proceso real, cada uno con su propio buzón de mensajes, sin el coste de un hilo del SO por actor.

La ejecución suele ser apropiativa: la máquina virtual decide cuándo interrumpir actores para dar paso a otros, basándose en criterios globales. Si el entorno dispone de varios núcleos, el runtime puede repartir actores entre hilos o núcleos de forma relativamente transparente, consiguiendo paralelismo efectivo.

Una consecuencia importante de este modelo es que los actores no comparten memoria mutable; toda comunicación se hace pasando mensajes (a menudo inmutables). Esto favorece la pureza funcional: un actor solo “ve” su estado y los datos que le llegan, lo que facilita razonar sobre el comportamiento y explotar paralelismo seguro. Además, es habitual un enfoque de manejo de errores optimista: si un actor falla por una condición transitoria, se reinicia sin tumbar el sistema completo.

Qué partes de un programa pueden ejecutarse concurrentemente

No todo el código se puede lanzar alegremente en paralelo o concurrencia. Hay fragmentos donde el orden de ejecución es crítico (por ejemplo, inicializar una variable antes de usarla), y otros donde el orden es irrelevante (dos cálculos independientes sobre datos distintos).

Para decidir si dos bloques de instrucciones pueden ejecutarse concurrentemente, se utilizan las condiciones de Bernstein. Para cada conjunto de instrucciones Sk definimos:

  • L(Sk): conjunto de variables leídas durante la ejecución de Sk.
  • E(Sk): conjunto de variables escritas (actualizadas) durante la ejecución de Sk.

Dos bloques Si ja Sj pueden ejecutarse de forma concurrente si se cumplen tres condiciones: E(Si) ∩ L(Sj) = ∅, L(Si) ∩ E(Sj) = ∅ y E(Si) ∩ E(Sj) = ∅. Es decir, ninguno de ellos debe escribir en una variable que el otro lea o escriba. Si se viola alguna de estas condiciones, ejecutar ambos bloques al mismo tiempo puede cambiar el resultado del programa.

Problemas típicos de los programas concurrentes

Diseñar programas concurrentes es complicado porque el orden exacto de ejecución de las acciones no está totalmente determinado. Ese no determinismo hace que aparezcan errores sutiles, a veces difíciles de reproducir, que solo se manifiestan bajo ciertas intercalaciones de instrucciones. Veamos los más comunes.

Condiciones de carrera y sección crítica

A. condición de carrera aparece cuando el resultado de una computación depende del orden relativo en que dos o más hilos acceden a recursos compartidos, y ese orden no está bajo control. Si varios hilos modifican la misma variable sin coordinación, el valor final puede ser incorrecto.

Un ejemplo clásico es la operación x = x + 1 sobre una variable compartida. En un programa secuencial es trivial, pero en un entorno multihilo cada incremento se compone de varias instrucciones: leer x, sumar 1, escribir el resultado. Si dos hilos intercalan estas instrucciones sin vastastikune välistamine, ambos pueden leer el mismo valor inicial y sobrescribirse, perdiendo incrementos.

En Java, un contador incrementado por dos hilos medio millón de veces cada uno debería terminar en un millón, pero en ausencia de sincronización se observan resultados distintos en cada ejecución. El motivo es que no existe un orden total de las operaciones, sino un orden parcial dependiente del planificador y de la intercalación concreta.

Para evitar estas situaciones se identifican las kriitilised lõigud, porciones de código que acceden a recursos compartidos y que no deben ser ejecutadas por más de un hilo a la vez. Mediante mecanismos de sincronización (locks, semáforos, monitores…) se garantiza que solo un hilo entra en la sección crítica, igual que un semáforo de tráfico controla el acceso a un cruce estrecho.

Violación de la exclusión mutua

Hablamos de violación de exclusión mutua cuando dos o más hilos consiguen entrar a la misma sección crítica simultáneamente. Esto rompe la garantía de acceso exclusivo al recurso compartido y da lugar a resultados indeseados, como el contador anterior que nunca llega al valor esperado.

Este tipo de fallos suele deberse a sincronización incompleta o incorrecta: olvidar proteger un acceso con un lock, usar instrucciones no atómicas, o combinar varios recursos sin una política clara de adquisición de bloqueos.

Deadlock o interbloqueo

Un ummikseis (interbloqueo o “abrazo mortal”) se produce cuando uno o varios procesos quedan esperando indefinidamente un evento que nunca va a ocurrir. Típicamente, cada proceso de un ciclo espera por un recurso que tiene el siguiente, creando una dependencia circular sin salida.

Para que exista deadlock en un sistema de recursos son necesarias cuatro condiciones simultáneas:

  • Vastastikune välistamine: los recursos se asignan de forma exclusiva (no compartida).
  • Retención y espera: los procesos mantienen recursos que ya poseen mientras solicitan otros adicionales.
  • No expropiación: los recursos no pueden retirarse a la fuerza de un proceso; solo se liberan voluntariamente.
  • Espera circular: existe una cadena circular de procesos donde cada uno espera un recurso retenido por el siguiente.
  Kuidas valida kogu tekst kiirklahvide abil: täielik juhend

Las técnicas para evitar o romper deadlocks se basan en negar al menos una de estas condiciones. Por ejemplo, ordenando globalmente los recursos para evitar ciclos, permitiendo ciertas formas de expropiación, o usando algoritmos de detección y recuperación. El algoritmo del banquero de Dijkstra es un clásico, así como su ejemplo de los “filósofos comensales”.

Aplazamiento indefinido e injusticia

El aplazamiento indefinido (starvation o inanición) ocurre cuando un proceso listo para ejecutarse o para obtener un recurso se retrasa indefinidamente porque la política de planificación nunca le da turno. No está bloqueado por un deadlock, simplemente siempre hay otros procesos que se cuelan delante de él.

Este fenómeno está muy ligado a la ebaõiglus (unfairness) en los algoritmos de planificación: si el sistema favorece sistemáticamente a ciertos procesos (por prioridad, por orden de llegada, etc.) sin compensar el tiempo de espera acumulado de los demás, es fácil que alguno se quede “a dos velas” para siempre.

Las soluciones típicas pasan por implementar mecanismos de envejecimiento (aumentar la prioridad de quien lleva mucho esperando) o tratar los procesos estrictamente en orden de espera. A nivel de diseño, es una guía: al escribir código concurrente hay que plantearse si el sistema garantiza que todos los hilos que pueden progresar acabarán haciéndolo.

Busy waiting u “ocupado esperando”

Otra trampa habitual es el busy waiting: un proceso entra en un bucle que revisa constantemente una condición (por ejemplo, el valor de una variable compartida) y no hace nada útil mientras tanto. Aunque no es incorrecto desde el punto de vista funcional, desperdicia CPU y puede provocar que otros procesos no reciban tiempo suficiente.

Lo ideal es que, cuando un proceso no pueda avanzar porque espera un evento, se suspenda y libere la CPU hasta que el evento tenga lugar. Esto se consigue mediante primitivas de bloqueo que duermen el hilo (wait, sleep, semáforos, condiciones de monitor, etc.), en lugar de dejarlo girando en un bucle.

False sharing

En arquitecturas modernas con caché, puede aparecer el problema de false sharing. Se da cuando varios hilos modifican variables distintas pero ubicadas en la misma línea de caché. Aunque conceptualmente son datos independientes, la caché invalida y sincroniza toda la línea cada vez que uno de ellos escribe, generando una tormenta de invalidaciones que degrada el rendimiento.

El false sharing no rompe la corrección del programa, pero provoca una caída importante en el rendimiento, sobre todo en bucles intensivos. Para mitigarlo se utilizan técnicas como la alineación de variables en memoria, el uso de padding (relleno) entre campos de estructuras o las opciones del compilador y runtime para separar los datos usados por distintos hilos en líneas de caché diferentes.

Propiedades de seguridad y de vida en programas concurrentes

Para razonar sobre si un programa concurrente “está bien hecho”, se suelen distinguir dos grandes familias de propiedades: turvalisus y elu (liveness). Ambas son fundamentales para definir el comportamiento correcto.

Propiedades de seguridad

Las propiedades de seguridad indican lo que el programa nunca debe hacer. Suelen formularse como invariantes: condiciones que deben mantenerse siempre verdaderas durante toda la ejecución.

  • Vastastikune välistamine: nunca debe haber más de un proceso en una sección crítica a la vez.
  • Ausencia de deadlock: ningún proceso debe quedar bloqueado esperando un evento que jamás llegará.
  • Correctitud parcial: si el programa termina, la salida obtenida debe ser una de las permitidas por la especificación.

Estas propiedades suelen comprobarse mediante razonamiento formal, pruebas sistemáticas o herramientas de verificación automática sobre modelos simplificados del sistema.

Propiedades de vida

Por otro lado, las propiedades de vida (liveness) capturan lo que el programa debe llegar a hacer en algún momento, es decir, que “algo bueno” ocurrirá eventualmente si no hay fallos externos.

  • Õiglus: todo proceso que está en condiciones de ejecutarse acabará teniendo su oportunidad.
  • Comunicación fiable: todo mensaje enviado termina siendo recibido por el destinatario (o, al menos, tratado según el protocolo de errores).
  • Correctitud total: si el programa termina, lo hace con el resultado correcto y no se queda bloqueado en mitad de la ejecución.

Garantizar propiedades de vida suele ser más difícil, ya que dependen tanto del diseño del algoritmo concurrente como de las políticas de planificación del sistema subyacente.

Problemas clásicos de concurrencia: lectores-escritores, productor-consumidor y filósofos

La teoría de concurrencia se apoya a menudo en problemas canónicos que sirven como banco de pruebas para técnicas de sincronización. Varios de ellos se han convertido en auténticos clásicos.

Problema lectores-escritores

En sistemas como bases de datos, ficheros compartidos o estructuras en memoria, es frecuente que existan múltiples lectores y múltiples escritores. Los lectores solo leen; los escritores modifican. Mientras nadie escriba, los lectores podrían acceder en paralelo sin problema, pero no se puede permitir que un escritor modifique los datos mientras otros leen o escriben, para evitar corrupción o lecturas inconsistentes.

Este problema exige diseñar algoritmos que otorguen acceso concurrente de lectura cuando no haya escrituras en curso, y acceso exclusivo para los escritores cuando necesiten actualizar. Dependiendo de la solución, se puede favorecer injustamente a lectores o escritores, lo que obliga a cuidar también las propiedades de vida.

Problema productor-consumidor

Otro escenario clásico es el de productores y consumidores que comparten un búfer (una “bodega” de ítems). Los productores generan elementos y los depositan en el búfer; los consumidores extraen esos elementos para procesarlos. El desafío es coordinar ambos lados para que los productores no desborden el búfer y los consumidores no intenten extraer de un búfer vacío.

Una solución sencilla consiste en proteger el búfer con exclusión mutua (por ejemplo, semáforos binarios) y usar contadores para bloquear a los productores cuando el búfer está lleno y a los consumidores cuando está vacío. Esta aproximación, aunque válida, puede volverse engorrosa cuando hay muchos actores o se complica el flujo.

Una alternativa más estructurada es encapsular el búfer y su lógica de sincronización en un jälgida: un objeto que ofrece operaciones atómicas (insertar, extraer) y gestiona internamente las colas de espera, evitando duplicar el código de sincronización en productores y consumidores.

La cena de los filósofos

Probleem cena de los filósofos ilustra muy bien interbloqueos, condiciones de carrera y inanición. Varios filósofos sentados alrededor de una mesa necesitan dos tenedores para comer, pero solo hay un tenedor entre cada par. Si todos intentan coger los tenedores a la vez, puede darse que cada uno coja uno distinto y se queden todos bloqueados esperando el segundo: un deadlock de manual.

Si no se diseña bien el algoritmo, también puede ocurrir que el mismo filósofo se quede siempre sin comer mientras otros se las apañan para acaparar tenedores de forma repetida, lo que ejemplifica la injusticia y la inanición.

Las soluciones propuestas van desde un ümmargune röövel sencillo (un sistema de turnos en el que comen uno a uno), pasando por colas de pensadores donde quien no consigue el segundo tenedor se va al final de la cola, hasta introducir un kohtunik que limita el número de filósofos que pueden sentarse a la mesa simultáneamente (por ejemplo, máximo n−1 si hay n tenedores), con lo que siempre habrá al menos un filósofo que pueda comer y liberar recursos.

Métodos de sincronización y comunicación entre procesos e hilos

Para mantener bajo control el acceso a zonas críticas y coordinar la ejecución de tareas concurrentes, disponemos de varias primitivas de sincronización y comunicación. Las más usadas en la práctica son los semáforos, locks, monitores y el paso de mensajes explícito.

Valgusfoorid

Un semáforo es una variable entera protegida a la que solo se puede acceder con dos operaciones atómicas: wait/down (decrementar y bloquearse si el valor es cero) y signal/up (incrementar y, opcionalmente, despertar un proceso bloqueado). El sistema operativo garantiza que estas operaciones son indivisibles, por lo que sirven para implementar exclusión mutua y otros patrones de sincronización.

  OBS jookseb salvestamise ajal kokku: põhjused, lahendused ja parandused

Un semáforo binario (solo 0 o 1) funciona como un lock; un semáforo de recuento permite un número máximo configurable de hilos en una sección determinada. Aunque son muy flexibles, los semáforos se consideran de bajo nivel de abstracción: mal usados son una fuente inagotable de bugs (deadlocks, olvidos de signal, etc.) y obligan a mezclar lógica de sincronización con la lógica del dominio.

Exclusión mutua mediante locks

La vastastikune välistamine se implementa habitualmente usando locks (mutual exclusion locks o mutex). Un proceso que quiere entrar en una sección crítica intenta adquirir el lock; si está libre, entra y lo marca como ocupado, si no, se bloquea hasta que el lock se libere.

Conceptualmente, es como un candado sobre el recurso: solo quien tiene la llave puede entrar en la sección crítica. Este patrón es muy común en la práctica, pero requiere disciplina: siempre hay que liberar el lock, usar el mismo orden de adquisición cuando hay varios, y evitar secciones críticas excesivamente largas que degraden el paralelismo.

Monitorid

Los monitores llevan la exclusión mutua a un plano más alto. Un monitor es una abstracción que agrupa datos compartidos y las operaciones que se pueden realizar sobre ellos, garantizando que esas operaciones se ejecutan una a una, nunca en paralelo dentro del monitor.

En un monitor, cuando un hilo llama a uno de sus métodos, entra automáticamente en una región protegida: si otro hilo ya está dentro, debe esperar su turno. Además, los monitores suelen ofrecer variables de condición (wait/notify, wait/signal) para suspender y despertar hilos en base a ciertas condiciones del estado interno, lo que simplifica la resolución de problemas como el productor-consumidor.

Como esta técnica tiene un alto nivel de abstracción, resulta más segura y menos propensa a errores que el uso directo de semáforos o locks dispersos por todo el código. Lenguajes como Java, C# o diversos runtimes modernos proporcionan soporte nativo para monitores y condiciones.

Comunicación mediante paso de mensajes

Otra familia de técnicas prescinde del estado compartido y se basa en sõnumeid vahetada entre procesos o hilos. Cada mensaje suele tener una cabecera (identificadores de emisor y receptor, tipo de mensaje, tamaño) y un cuerpo con los datos necesarios.

Hay varios modos de direccionamiento: en el direccionamiento directo, el emisor indica explícitamente quién es el receptor y ambos se conocen mutuamente. En el direccionamiento implícito, el emisor especifica el receptor, pero este no sabe necesariamente quién le envía el mensaje. Por último, en el kaudne marsruutimine los mensajes se depositan en buzones (mailboxes) gestionados por procesos concretos o por el sistema operativo; los procesos interesados leen del buzón para saber si pueden acceder a una determinada sección crítica o procesar cierto evento.

La sincronización puede ser bloqueante (el emisor espera a que el receptor reciba, o el receptor espera a que haya mensaje) o no bloqueante. Elegir qué combinaciones usar es crucial: una mala elección puede crear nuevas condiciones de carrera o deadlocks en vez de resolverlas.

Planificación de procesos y cambios de contexto

La protsessi planeerimine es la estrategia mediante la cual el sistema operativo decide qué proceso u hilo se ejecuta en cada momento, compartiendo la CPU entre todos los que están en memoria. Se basa en manejar colas de procesos listos y aplicar algoritmos de scheduling con objetivos como maximizar la utilización de CPU y minimizar retardos (ver las novedades del nuevo kernel).

Se suele hablar de tres niveles de calendarización:

  • Lühiajaline: decide qué proceso en estado listo obtiene la CPU a continuación. Controla directamente el reparto inmediato de tiempo de procesador.
  • Keskpika perioodi: también llamado CPU scheduler o dispatcher, decide qué procesos deben permanecer en memoria principal y cuáles pueden ser desplazados (swapped) para optimizar el rendimiento global. La dispatch latency es el tiempo que tarda el sistema en parar un proceso y arrancar otro.
  • Pikaajaline: regula qué procesos se aceptan en el sistema y cuáles se sacan de memoria, afectando al nivel de multiprogramación global (cuántos procesos hay activos al mismo tiempo).

Los algoritmos clásicos de planificación incluyen FCFS (First-Come, First-Served), SJF (Shortest-Job-First), planificación por prioriteetidele, Round Robini y colas multinivel. Cada uno ofrece diferentes compromisos en términos de tiempo de respuesta, throughput, equidad y complejidad.

Para alternar entre procesos es necesario realizar konteksti muutused: el sistema guarda el contenido de los registros, el puntero de pila y el contador de programa del proceso saliente en su PCB, y restaura los del proceso entrante. Este mecanismo permite que un proceso se reanude exactamente donde lo dejó, como si nada hubiera pasado entre medias.

Herramientas de verificación de sistemas concurrentes

Dado que el comportamiento de un sistema concurrente puede variar enormemente según el orden de los eventos, es habitual recurrir a modelos formales y herramientas de verificación que ayuden a detectar deadlocks, condiciones de carrera y otras violaciones de propiedades deseadas antes de desplegar el sistema real.

Redes de Petri

The redes de Petri son un modelo gráfico para describir sistemas concurrentes. Se representan como un grafo dirigido y bipartito con dos tipos de vértices: kohad (condiciones) y üleminekud (eventos). Los lugares contienen fichas (tokens) que indican qué condiciones se cumplen en un estado dado.

Una transición está lubatud si todos sus lugares de entrada tienen al menos una ficha. Cuando se dispara una transición, consume una ficha de cada lugar de entrada y añade fichas en sus lugares de salida. A partir de un marcado inicial (distribución de fichas), es posible estudiar propiedades como si el sistema está vivo (toda transición puede dispararse eventualmente) o acotado/seguro (no se acumulan fichas infinitamente en un lugar).

CSP (järjestikuste protsesside edastamine)

CSP es una teoría matemática para especificar y razonar sobre sistemas compuestos por procesos secuenciales que se comunican mediante canales. Su semántica formal permite describir patrones de interacción, detectar interbloqueos (deadlocks) o livelocks y verificar que un diseño cumple ciertas propiedades de seguridad y vida.

En CSP, los procesos se conectan a través de ühesuunalised kanalid y sincronizan usando un mecanismo de rendezvous: la escritura y la lectura de un mensaje en un canal se consideran acciones conjuntas que se producen simultáneamente, evitando ciertas formas de condiciones de carrera asociadas al estado compartido tradicional.

FDR y JCSP

FDR (Failures-Divergence Refinement) es una herramienta basada en CSP que permite comprobar automáticamente si un modelo de sistema cumple determinadas propiedades expresadas también como procesos CSP. La idea es verificar si el sistema real es un täpsustamine (no introduce nuevas fallas o divergencias) del modelo abstracto deseado, detectando, por ejemplo, violaciones de seguridad o posibles deadlocks.

Omalt JCSP es una biblioteca para Java que implementa directamente los conceptos de CSP: procesos, canales, sincronización, etc. Facilita escribir programas concurrentes en Java sin recurrir a locks y compartición de memoria directa, apoyándose en primitivas de comunicación seguras y bien definidas como channels, barriers, timers y otros componentes de alto nivel.

En conjunto, todo este ecosistema de modelos, problemas clásicos, primitivas de sincronización y herramientas de verificación proporciona un marco sólido para diseñar y analizar programas concurrentes fiables, eficientes y escalables, capaces de sacar partido tanto a la intercalación de tareas como al paralelismo disponible en el hardware moderno.

Linux 6.19
Seotud artikkel:
Linux 6.19, kõik uue kerneli uued funktsioonid ja täiustused