En la programación concurrente , los accesos simultáneos a recursos compartidos pueden dar lugar a un comportamiento inesperado o erróneo. Por lo tanto, las partes del programa en las que se accede al recurso compartido deben protegerse de forma que se evite el acceso simultáneo. Una forma de hacerlo se conoce como sección crítica o región crítica . A esta sección protegida no puede acceder más de un proceso o hilo a la vez; los demás se suspenden hasta que el primero abandona la sección crítica. Normalmente, la sección crítica accede a un recurso compartido, como una estructura de datos , un dispositivo periférico o una conexión de red, que no funcionaría correctamente en el contexto de múltiples accesos simultáneos. [1]
Diferentes códigos o procesos pueden estar compuestos por la misma variable u otros recursos que deben leerse o escribirse, pero cuyos resultados dependen del orden en que se producen las acciones. Por ejemplo, si el proceso A debe leer una variable x y el proceso B debe escribir en la variable x al mismo tiempo, el proceso A puede obtener el valor anterior o el nuevo de x .
Proceso A:
// Proceso A . . b = x + 5 ; // la instrucción se ejecuta en el tiempo = Tx .
Proceso B:
// Proceso B . . x = 3 + z ; // la instrucción se ejecuta en el tiempo = Tx .
En los casos en los que no se necesita un mecanismo de bloqueo con una granularidad más fina, es importante una sección crítica. En el caso anterior, si A necesita leer el valor actualizado de x , la ejecución del proceso A y del proceso B al mismo tiempo puede no dar los resultados requeridos. Para evitar esto, la variable x está protegida por una sección crítica. Primero, B obtiene el acceso a la sección. Una vez que B termina de escribir el valor, A obtiene el acceso a la sección crítica y se puede leer la variable x .
Al controlar cuidadosamente qué variables se modifican dentro y fuera de la sección crítica, se evita el acceso simultáneo a la variable compartida. Una sección crítica se utiliza normalmente cuando un programa multiproceso debe actualizar varias variables relacionadas sin que un subproceso independiente realice cambios conflictivos en esos datos. En una situación relacionada, una sección crítica se puede utilizar para garantizar que un recurso compartido, por ejemplo, una impresora, solo pueda ser accedido por un proceso a la vez.
La implementación de secciones críticas varía entre los diferentes sistemas operativos.
Una sección crítica normalmente terminará en un tiempo finito, [2] y un hilo, tarea o proceso debe esperar un tiempo fijo para ingresar a ella ( espera limitada ). Para garantizar el uso exclusivo de las secciones críticas, se requiere algún mecanismo de sincronización en la entrada y salida del programa.
Una sección crítica es una parte de un programa que requiere exclusión mutua de acceso.
Como se muestra en la figura [3] , en el caso de exclusión mutua ( mutex ), un subproceso bloquea una sección crítica mediante técnicas de bloqueo cuando necesita acceder al recurso compartido, y los demás subprocesos deben esperar su turno para ingresar a la sección. Esto evita conflictos cuando dos o más subprocesos comparten el mismo espacio de memoria y desean acceder a un recurso común. [2]
El método más simple para evitar cualquier cambio de control del procesador dentro de la sección crítica es implementar un semáforo. En sistemas monoprocesador, esto se puede hacer deshabilitando las interrupciones al ingresar a la sección crítica, evitando llamadas al sistema que puedan causar un cambio de contexto mientras se está dentro de la sección y restaurando las interrupciones a su estado anterior al salir. Con esta implementación, cualquier hilo de ejecución que ingrese a cualquier sección crítica del sistema evitará que cualquier otro hilo, incluida una interrupción, obtenga tiempo de procesamiento en la CPU hasta que el hilo original abandone su sección crítica.
Este enfoque de fuerza bruta se puede mejorar mediante el uso de semáforos . Para ingresar a una sección crítica, un subproceso debe obtener un semáforo, que libera al salir de la sección. Se evita que otros subprocesos ingresen a la sección crítica al mismo tiempo que el subproceso original, pero son libres de obtener el control de la CPU y ejecutar otro código, incluidas otras secciones críticas que están protegidas por diferentes semáforos. El bloqueo de semáforos también tiene un límite de tiempo para evitar una condición de bloqueo en la que un solo proceso adquiere un bloqueo durante un tiempo infinito, lo que detiene a los otros procesos que necesitan usar el recurso compartido protegido por la sección crítica.
Normalmente, las secciones críticas impiden la migración de subprocesos y procesos entre procesadores y la interrupción y otros procesos y subprocesos impiden el paso de procesos y subprocesos.
Las secciones críticas suelen permitir la anidación, lo que permite entrar y salir de varias secciones críticas con un coste reducido.
Si el programador interrumpe el proceso o subproceso actual en una sección crítica, permitirá que el proceso o subproceso que se está ejecutando actualmente se ejecute hasta completar la sección crítica o programará el proceso o subproceso para otro quantum completo. El programador no migrará el proceso o subproceso a otro procesador y no programará otro proceso o subproceso para que se ejecute mientras el proceso o subproceso actual se encuentre en una sección crítica.
De manera similar, si se produce una interrupción en una sección crítica, la información de la interrupción se registra para su posterior procesamiento y la ejecución se devuelve al proceso o subproceso en la sección crítica. [4] Una vez que se sale de la sección crítica y, en algunos casos, se completa el quantum programado, se ejecutará la interrupción pendiente. El concepto de quantum de programación se aplica a las políticas de programación " round-robin " y similares .
Dado que las secciones críticas solo pueden ejecutarse en el procesador en el que se ingresan, la sincronización solo se requiere dentro del procesador en ejecución. Esto permite ingresar y salir de las secciones críticas casi sin costo. No se requiere sincronización entre procesadores. Solo se necesita la sincronización del flujo de instrucciones. [5] La mayoría de los procesadores proporcionan la cantidad necesaria de sincronización interrumpiendo el estado de ejecución actual. Esto permite que las secciones críticas en la mayoría de los casos no sean más que un recuento por procesador de secciones críticas ingresadas.
Las mejoras de rendimiento incluyen la ejecución de interrupciones pendientes a la salida de todas las secciones críticas y la posibilidad de que el programador se ejecute a la salida de todas las secciones críticas. Además, las interrupciones pendientes se pueden transferir a otros procesadores para su ejecución.
Las secciones críticas no se deben utilizar como un bloqueo primitivo de larga duración. Las secciones críticas deben ser lo suficientemente breves para que se pueda ingresar, ejecutar y salir de ellas sin que se produzcan interrupciones del hardware y del programador.
Las secciones críticas a nivel de kernel son la base del problema de bloqueo del software .
En la programación paralela, el código se divide en subprocesos. Las variables conflictivas de lectura y escritura se dividen entre subprocesos y cada subproceso tiene una copia de ellas. Las estructuras de datos como listas enlazadas , árboles y tablas hash tienen variables de datos que están enlazadas y no se pueden dividir entre subprocesos; por lo tanto, implementar el paralelismo es muy difícil. [6] Para mejorar la eficiencia de la implementación de estructuras de datos, se pueden ejecutar en paralelo múltiples operaciones como inserción, eliminación y búsqueda. Al realizar estas operaciones, puede haber escenarios en los que un subproceso busque el mismo elemento y otro lo elimine. En tales casos, la salida puede ser errónea . El subproceso que busca el elemento puede tener un acierto, mientras que el otro subproceso puede eliminarlo posteriormente. Estos escenarios causarán problemas en la ejecución del programa al proporcionar datos falsos. Para evitar esto, un método es mantener toda la estructura de datos en la sección crítica para que solo se maneje una operación a la vez. Otro método es bloquear el nodo en uso en la sección crítica, para que otras operaciones no utilicen el mismo nodo. De esta forma, el uso de la sección crítica garantiza que el código proporcione los resultados esperados. [6]
Las secciones críticas también se producen en el código que manipula periféricos externos, como dispositivos de E/S. Los registros de un periférico deben programarse con determinados valores en una secuencia determinada. Si dos o más procesos controlan un dispositivo simultáneamente, ninguno de los procesos tendrá el dispositivo en el estado que requiere y se producirá un comportamiento incorrecto.
Cuando se debe producir una unidad compleja de información en un dispositivo de salida mediante la emisión de múltiples operaciones de salida, se requiere acceso exclusivo para que otro proceso no corrompa los datos intercalando sus propios bits de salida.
En la dirección de entrada, se requiere acceso exclusivo cuando se lee un dato complejo mediante múltiples operaciones de entrada independientes. Esto evita que otro proceso consuma algunas de las partes, lo que causaría corrupción.
Los dispositivos de almacenamiento proporcionan una forma de memoria. El concepto de secciones críticas es igualmente relevante para los dispositivos de almacenamiento como para las estructuras de datos compartidas en la memoria principal. Un proceso que realiza múltiples operaciones de acceso o actualización en un archivo está ejecutando una sección crítica que debe protegerse con un mecanismo de bloqueo de archivos adecuado.
{{cite book}}
: CS1 maint: varios nombres: lista de autores ( enlace ){{cite journal}}
: CS1 maint: varios nombres: lista de autores ( enlace )