¿Está desarrollando una aplicación multiproceso? Tarde o temprano, probablemente necesitará utilizar un semáforo. En este artículo, aprenderá qué es un semáforo, cómo crear / llevar a la práctica uno en Bash y más.
Que es un Semáforo?
Un semáforo es una construcción de programación que se utiliza en programas de computadora que emplean múltiples subprocesos de procesamiento (subprocesos de procesamiento de computadora que ejecutan código fuente desde el mismo programa o el mismo conjunto de programas) para lograr el uso exclusivo de un recurso común en un momento dado. . Dicho de una manera mucho más sencilla, piense en esto como «uno al mismo tiempo, por favor».
Un semáforo fue definido por primera vez a finales de la década de 1960 por el difunto científico informático Edsger Dijkstra de Rotterdam en los Países Bajos. ¡Es probable que haya usado semáforos frecuentemente en su vida sin darse cuenta específicamente de que lo estaba haciendo!
Quedarme en los Países Bajos por un tiempo, un país lleno de pequeñas vías fluviales y muchos puentes móviles (llamado puente levadizo en inglés americano), se pueden ver muchos ejemplos excelentes del mundo real de un semáforo; considere la manija de un operador de puente levadizo: hacia arriba o hacia abajo. Esa manija es la variable de semáforo que protege la vía fluvial o la carretera de accidentes. Así, la vía fluvial y las carreteras podrían verse como otras variables protegidas por el semáforo.
Si la manija está hacia arriba y el puente está abierto, el uso exclusivo de la intersección agua / carretera se le da al barco o barcos que pasan por el canal de agua. Cuando la manija está hacia abajo, el puente se cierra y se da el uso exclusivo de la intersección agua / carretera a los autos que pasan por el puente.
La variable de semáforo puede controlar el acceso a otro conjunto de variables. A modo de ejemplo, el estado de la manija evita que el $cars_per_minute
y $car_toll_booth_total_income
variables para que no se actualicen, etc.
Podemos llevar el ejemplo un poco más lejos y explicar que cuando la manija funciona hacia arriba o hacia abajo, tanto los capitanes de los barcos en el agua como las personas que conducen en automóviles y camiones en la carretera pueden ver una luz coincidente: todos los operadores pueden leer una variable común para un estado específico.
Aún cuando el escenario descrito aquí no es solo un semáforo, sino que al mismo tiempo es un simple mutex. Un mutex es otra construcción de programación común que es muy semejante a un semáforo, con la condición adicional de que un mutex solo puede ser desbloqueado por la misma tarea o subproceso que lo bloqueó. Mutex significa «mutuamente excluyentes».
Para este caso, esto se aplica a nuestro ejemplo, dado que el operador de puente es el único con control sobre nuestro semáforo y mutex arriba / abajo. Por el contrario, si la caseta de vigilancia al final del puente tuviera un interruptor de anulación de puente, aún tendríamos una configuración de semáforo, pero no un mutex.
Ambas construcciones se usan con regularidad en la programación de computadoras cuando se usan múltiples subprocesos para garantizar que solo un único procedimiento o tarea acceda a un recurso determinado en cualquier momento. Algunos semáforos pueden ser de conmutación ultrarrápida, a modo de ejemplo, cuando se emplean en software de negociación de mercados financieros multiproceso, y algunos pueden ser mucho más lentos, solo cambian de estado cada pocos minutos, como cuando se usan en un puente levadizo automatizado o en un cruce de trenes.
Ahora que tenemos una mejor comprensión de los semáforos, implementemos uno en Bash.
Llevar a la práctica un semáforo en Bash: ¿fácil o no?
Llevar a la práctica un semáforo en Bash es tan fácil que inclusive se puede hacer de forma directa desde la línea de comandos, o eso parece …
Comencemos de forma simple.
BRIDGE=up if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi BRIDGE=down if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi
En este código, la variable BRIDGE
tiene nuestro estatus de puente. Cuando lo ponemos en up
, los barcos pueden pasar y cuando lo ponemos en down
, los coches pueden pasar. Al mismo tiempo podríamos leer el valor de nuestra variable en cualquier punto para ver si el puente está verdaderamente hacia arriba o hacia abajo. El recurso compartido / común, en esta circunstancia, es nuestro puente.
A pesar de ello, este ejemplo es de un solo subproceso y, es por ello que, nunca encontramos comose necesita un emaphore situación. Otra manera de pensar en esto es que nuestra variable nunca puede ser up
y down
exactamente en el mismo momento en que el código se ejecuta secuencialmente, dicho de otra manera, paso a paso.
Otra cosa a prestar atención es que verdaderamente no controlamos el acceso a otra variable (como lo haría regularmente un semáforo), por lo que nuestro BRIDGE
variable no es verdaderamente una variable de semáforo verdadera, aún cuando se acerca.
En resumen, tan pronto como introducimos varios subprocesos que pueden afectar el BRIDGE
variable nos encontramos con problemas. A modo de ejemplo, ¿y si, de forma directa después de la BRIDGE=up
comando, otro hilo problemas BRIDGE=down
que posteriormente daría como consecuencia el mensaje Cars may pass!
salida, aún cuando el primer hilo esperaría que el puente fuera up
, y en realidad el puente aún se está moviendo. ¡Peligroso!
Puede ver cómo las cosas pueden volverse rápidamente turbias y confusas, por no mencionar complejas, cuando se trabaja con bastantes subprocesos.
La situación en la que varios subprocesos intentan actualizar la misma variable al mismo tiempo o al menos lo suficientemente cerca en el tiempo para que otro subproceso se equivoque en la situación (que en el caso de los puentes levadizos puede tardar bastante) se denomina condición de carrera: dos subprocesos compitiendo para actualizar o informar alguna variable o estado, con el resultado de que uno o más subprocesos pueden equivocarse bastante.
Podemos mejorar mucho este código haciendo fluir el código en procedimientos y usando una variable de semáforo real que restringirá el acceso a nuestra BRIDGE
variable según la situación.
Creación de un semáforo Bash
Llevar a la práctica un multiproceso completo, que sea seguro para subprocesos (una definición informático para describir el software que es seguro para subprocesos o desarrollado de tal manera que los subprocesos no pueden afectarse negativa o incorrectamente entre sí cuando no deberían) no es una tarea fácil. Inclusive un programa bien escrito que emplea semáforos no está garantizado para ser absolutamente seguro para subprocesos.
Cuantos más subprocesos haya, y cuanto mayor sea la frecuencia y complejidad de las interacciones entre subprocesos, es más probable que haya condiciones de carrera.
Para nuestro pequeño ejemplo, veremos la definición de un semáforo Bash cuando uno de los operadores del puente levadizo baja la manija del puente, lo que indica que quiere bajar el puente. Los lectores ávidos pueden haber notado la referencia a operadors en lugar de operador: ahora hay varios operadores que pueden bajar el puente. Dicho de otra manera, hay varios subprocesos o tareas que se ejecutan al mismo tiempo.
#!/bin/bash BRIDGE_SEMAPHORE=0 lower_bridge(){ # An operator put one of the bridge operation handles downward (as a new state). # Assume it was previously agreed between operators that as soon as one of the operators # moves a bridge operation handle that their command has to be executed, either sooner or later # hence, we commence a loop which will wait for the bridge to become available for movement while true; do if [ "${BRIDGE_SEMAPHORE}" -eq 1 ]; then echo "Bridge semaphore locked, bridge moving or other issue. Waiting 2 minutes before re-check." sleep 120 continue # Continue loop elif [ "${BRIDGE_SEMAPHORE}" -eq 0 ]; then echo "Lower bridge command accepted, locking semaphore and lowering the bridge." BRIDGE_SEMAPHORE=1 execute_lower_bridge wait_for_bridge_to_come_down BRIDGE='down' echo "Bridge lowered, ensuring at least 5 minutes pass before next allowed bridge movement." sleep 300 echo "5 Minutes passed, unlocking semaphore (releasing bridge control)" BRIDGE_SEMAPHORE=0 break # Exit loop fi done }
Aquí tenemos un lower_bridge
función que hará una serie de cosas. En primer lugar, supongamos que otro operador ha movido recientemente el puente en el último minuto. Como tal, hay otro subproceso que ejecuta código en una función semejante a esta llamada raise_bridge
.
En realidad, esa función ha terminado de levantar el puente, pero ha instituido una espera obligatoria de 5 minutos que todos los operadores acordaron previamente y que estaba codificada en el código fuente: evita que el puente suba / baje todo el tiempo. Al mismo tiempo puede ver esta espera obligatoria de 5 minutos implementada en esta función como sleep 300
.
Entonces, cuando eso raise_bridge
la función está operando, habrá establecido la variable de semáforo BRIDGE_SEMAPHORE
a 1, de la misma forma que lo hacemos en el código aquí (de forma directa después del echo "Lower bridge command accepted, locking semaphore and lowering bridge"
comando), y – por medio del primer if
verificación condicional en este código: el bucle infinito presente en esta función continuará (ref continue
en el código) para realizar un bucle, con pausas de 2 minutos, como el BRIDGE_SEMAPHORE
variable es 1.
Tan pronto como eso raise_bridge
La función termina de levantar el puente y termina sus cinco minutos de suspensión, establecerá el BRIDGE_SEMAPHORE
a 0, permitiendo que nuestro lower_bridge
función co comienza a ejecutar las funciones execute_lower_bridge
y subsecuente wait_for_bridge_to_come_down
al mismo tiempo que hemos vuelto a bloquear nuestro semáforo para 1
para evitar que otras funciones asuman el control del puente.
A pesar de ello, existen deficiencias en este código y son posibles condiciones de carrera que pueden tener consecuencias de gran alcance para los operadores de puentes. ¿Puedes ver alguno?
los "Lower bridge command accepted, locking semaphore and lowering bridge"
no es seguro para subprocesos.
Si otro hilo, a modo de ejemplo raise_bridge
se está ejecutando al mismo tiempo e intentando tener acceso al BRIDGE_SEMAPHORE
variable, podría ser (cuando BRIDGE_SEMAPHORE=0
y ambos hilos en ejecución alcanzan sus respectivos echo
es exactamente al mismo tiempo que los operadores del puente ven «Se acepta el comando de puente inferior, se bloquea el semáforo y se baja el puente» y «Se acepta el comando de elevación del puente, se bloquea el semáforo y se eleva el puente». ¡De forma directa uno tras otro en la pantalla! Miedo, ¿no?
Más aterrador aún es el hecho de que ambos hilos pueden continuar BRIDGE_SEMAPHORE=1
, ¡y ambos subprocesos pueden continuar ejecutándose! (No hay nada que les impida hacerlo) El motivo es que aún no hay mucha protección para tales escenarios. Aún cuando este código implementa un semáforo, de ninguna manera es seguro para subprocesos. Como se dijo, la codificación de subprocesos múltiples es compleja y necesita mucha experiencia.
Aún cuando el tiempo requerido en esta circunstancia es mínimo (1-2 líneas de código toman solo unos pocos milisegundos para ejecutarse), y dado el número probablemente bajo de operadores de puentes, la posibilidad de que esto suceda es muy pequeña. A pesar de ello, el hecho de que sea viable es lo que lo hace peligroso. Crear código seguro para subprocesos en Bash no es una tarea fácil.
Esto podría mejorarse aún más, a modo de ejemplo, introduciendo un prebloqueo y / o introduciendo algún tipo de retraso con una nueva verificación posterior (aún cuando esto probablemente requerirá una variable adicional) o haciendo una nueva verificación regular antes de la ejecución real del puente. , etc. Otra alternativa es hacer una cola de prioridad o una variable de contador que verifique cuántos subprocesos han bloqueado el control del puente, etc.
Otro enfoque comúnmente utilizado, a modo de ejemplo, cuando se ejecutan varios scripts de bash que podrían interactuar, es usar mkdir
o flock
como operaciones de bloqueo de base. Hay varios ejemplos de cómo implementarlos disponibles en línea, a modo de ejemplo, ¿Qué comandos de Unix se pueden usar como semáforo / bloqueo?.
Terminando
En este artículo, echamos un vistazo a lo que es un semáforo. Al mismo tiempo tocamos brevemente el tema de un mutex. En resumen, analizamos la implementación de un semáforo en Bash usando el ejemplo práctico de varios operadores de puentes que operan un puente móvil / puente levadizo. Al mismo tiempo exploramos cuán compleja es la implementación de una solución confiable basada en semáforos.
Si disfrutó leyendo este artículo, eche un vistazo a nuestras afirmaciones, errores y bloqueos: ¿cuál es la diferencia? artículo.