Cómo crear un semáforo en Bash

Contenidos

Bash Shell

¿Está desarrollando una aplicación multiproceso? Tarde o temprano, probablemente necesitará usar un semáforo. En este post, aprenderá qué es un semáforo, cómo crear / poner en 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 usualmente 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.

Aunque el escenario descrito aquí no es solo un semáforo, sino que además es un simple mutex. Un mutex es otra construcción de programación común que es muy equivalente 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.

Poner en práctica un semáforo en Bash: ¿fácil o no?

Poner en práctica un semáforo en Bash es tan simple 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

Ejemplo de línea de comando de actualización y salida del estado del puente

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. Además 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 esto, este ejemplo es de un solo subproceso y, por eso, 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 forma, 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.

Por último, 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 muchos 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

Poner en 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 totalmente 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 forma, 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
}

Una implementación de semáforo en Bash

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 equivalente 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. Además 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, del mismo modo 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 esto, 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 entrar al BRIDGE_SEMAPHORE variable, podría ser (cuando BRIDGE_SEMAPHORE=0 y ambos hilos en ejecución alcanzan sus respectivos echoes 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. Aunque 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.

Aunque 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 oportunidad de que esto suceda es muy pequeña. A pesar de esto, el hecho de que sea factible 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 utilizar 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 post, echamos un vistazo a lo que es un semáforo. Además tocamos brevemente el tema de un mutex. Por último, 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. Además exploramos cuán compleja es la implementación de una solución confiable basada en semáforos.

Si disfrutó leyendo este post, eche un vistazo a nuestras afirmaciones, errores y bloqueos: ¿cuál es la diferencia? post.

Suscribite a nuestro Newsletter

No te enviaremos correo SPAM. Lo odiamos tanto como tú.