Semaphore es un primitivo de sincronización que controla el acceso a un recurso compartido mediante un contador y una cola de hilos en espera. Según Wikipedia, 2024, el semáforo fue propuesto por Edsger Dijkstra en 1965 para resolver problemas de interacción multihilo. La herramienta permite limitar el número de hilos que trabajan simultáneamente con una sección crítica.
Puntos clave
Semaphore es un primitivo de sincronización que utiliza un contador para controlar el acceso a un recurso compartido. El concepto fue propuesto por Edsger Dijkstra en 1965 y se convirtió en la base de todos los mecanismos modernos de sincronización en sistemas operativos.
Un semáforo es una variable entera con dos operaciones atómicas: wait (acquire) y signal (release). La operación wait disminuye el contador, mientras que signal lo aumenta. Cuando el contador llega a cero, el hilo que llama a wait se bloquea hasta que otro hilo ejecuta signal.
El propósito principal de un semáforo es proteger las secciones críticas del acceso simultáneo de múltiples hilos. A diferencia de un mutex, un semáforo no requiere vinculación a un hilo propietario, lo que lo hace adecuado para una gama más amplia de tareas de coordinación.
El concepto del semáforo surgió en el contexto del sistema operativo THE, desarrollado en la Technische Hogeschool Eindhoven. Dijkstra formalizó el semáforo como una abstracción matemática, demostrando su suficiencia para implementar cualquier primitivo de sincronización.
El mecanismo del semáforo se basa en dos operaciones atómicas y una cola de espera interna. Cuando se llama a acquire, el hilo verifica el valor del contador y, o continúa la ejecución, o se bloquea hasta que se libera el recurso.
Al crear un semáforo, se establece un valor inicial del contador de permisos. Cada llamada a acquire disminuye el contador en 1. Si después de esto el contador se vuelve negativo, el hilo se bloquea. La operación release aumenta el contador y despierta a uno de los hilos en espera.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} está trabajando")
} finally {
semaphore.release()
}
}
Cuando un hilo llama a acquire con un contador en cero, el SO lo coloca en la cola FIFO del semáforo. El hilo pasa al estado BLOCKED, sin consumir tiempo de CPU. Después de una llamada a release, el primer hilo en la cola pasa al estado RUNNABLE y obtiene acceso al recurso.
En la teoría de sincronización se distinguen dos tipos principales de semáforos: binario y contador. La elección del tipo depende de la tarea específica de gestión de acceso a recursos.
Un semáforo binario solo toma valores 0 y 1. En su comportamiento se asemeja a un mutex, pero sin el requisito de propiedad: cualquier hilo puede ejecutar release. Estos semáforos son convenientes para implementar banderas de disponibilidad y eventos entre hilos.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Datos listos")
}
Un semáforo contador puede tomar cualquier valor no negativo. Se utiliza para gestionar un conjunto de recursos similares donde hay varias instancias disponibles. Por ejemplo, un grupo de 5 conexiones de red: cada acquire toma una conexión, release la devuelve al grupo.
Los semáforos contadores son indispensables para limitar la velocidad de acceso a servicios externos e implementar grupos de hilos. Permiten controlar con precisión el grado de paralelismo sin gestión manual de hilos.
| Parámetro | Semáforo binario | Semáforo contador |
|---|---|---|
| Rango | 0 o 1 | de 0 a N |
| Hilos simultáneos | 1 | hasta N |
| Aplicación | señalización, banderas | grupos de recursos, limitación de velocidad |
Los desarrolladores a menudo confunden semáforo y mutex, aunque existen diferencias fundamentales entre ellos. Comprender estas diferencias es críticamente importante para elegir el mecanismo de sincronización correcto en un proyecto.
La diferencia clave es el concepto de propiedad. Un mutex siempre sabe qué hilo lo ha adquirido, y solo ese hilo puede liberarlo. Un semáforo no tiene propietario: cualquier hilo puede llamar a release sin siquiera haber llamado a acquire. Esto hace que el mutex sea más seguro para la protección de datos y el semáforo más flexible para la coordinación.
En la práctica, un mutex es más rápido para la exclusión mutua simple gracias a las optimizaciones para escenarios típicos. Un semáforo requiere una sobrecarga adicional para mantener el contador. Sin embargo, para limitar el paralelismo o implementar el patrón productor-consumidor, un semáforo es indispensable.
| Característica | Semaphore | Mutex |
|---|---|---|
| Propiedad | sin propietario | tiene propietario |
| Liberación | cualquier hilo | solo el hilo propietario |
| Contador | de 0 a N | binario |
| Caso de uso | limitación de paralelismo y señalización | protección de sección crítica |
| Recursión | no | sí (reentrante) |
En el desarrollo de aplicaciones móviles, Semaphore se utiliza para gestionar el acceso a recursos limitados: conexiones de red, archivos, bases de datos y componentes de hardware. Las plataformas modernas proporcionan implementaciones integradas convenientes.
Un caso de uso típico es un grupo de conexiones HTTP. Una aplicación puede enviar no más de 4 solicitudes simultáneas a un servidor porque la API del proveedor limita el paralelismo. Un semáforo con un valor inicial de 4 garantiza que bajo cualquier carga, el número de solicitudes concurrentes no exceda el límite, mientras que otros hilos esperan en la cola.
Sin un semáforo, un aumento brusco de la actividad del usuario podría causar una sobrecarga repentina en la infraestructura del servidor, lo que lleva a tiempos de espera y errores 429 Too Many Requests. Semaphore actúa como un fusible, permitiendo estrictamente un número especificado de llamadas concurrentes independientemente del número de hilos activos.
Android proporciona la clase Semaphore del paquete java.util.concurrent. Veamos un ejemplo de limitación de solicitudes de red concurrentes a dos hilos para evitar la sobrecarga del servidor.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
En iOS, DispatchSemaphore de GCD resuelve la misma tarea. Los desarrolladores lo utilizan para sincronizar el acceso a recursos en código asíncrono sin bloquear el hilo principal.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
El error más común es un release olvidado cuando ocurre una excepción. Si un hilo termina con un error antes de llamar a release, el semáforo permanece bloqueado permanentemente para otros hilos. Use try/finally o defer para garantizar la liberación. El segundo problema es el deadlock al adquirir múltiples semáforos en diferente orden por diferentes hilos.
Los semáforos no solo se utilizan para la protección de datos, sino también para la coordinación de hilos en escenarios multihilo complejos. Conocer los patrones comunes acelera el desarrollo y reduce la probabilidad de errores de sincronización.
Existen varios patrones probados de uso de semáforos en proyectos reales. Conocerlos ayuda a evitar errores típicos y construir sistemas multihilo fiables.
Un semáforo con un valor inicial de N y liberación periódica mediante un temporizador implementa la limitación de velocidad de solicitudes a una API. Por ejemplo, un servicio permite 10 solicitudes por segundo: el semáforo comienza en 10, cada solicitud disminuye el contador, y un TimerTask separado devuelve el contador a su valor inicial cada segundo. Esto protege tanto a la aplicación como al servidor de la sobrecarga.
En el problema clásico del productor-consumidor, dos semáforos gestionan un búfer: empty (permisos de escritura) y full (permisos de lectura). El productor llama a acquire en empty y release en full, mientras que el consumidor hace lo contrario. Este esquema garantiza que el consumidor nunca lea un búfer vacío y el productor nunca lo desborde.
Este mismo esquema subyace al búfer acotado en los sistemas operativos — un búfer circular de tamaño fijo. En aplicaciones móviles, el patrón se utiliza para procesar colas de imágenes, archivos de video y eventos analíticos.
Los semáforos se utilizan con éxito para la limitación de llamadas de red en servicios en segundo plano. Por ejemplo, una aplicación de análisis envía paquetes de eventos al servidor. Sin limitar los hilos concurrentes durante picos de carga (inicio de la aplicación, sincronización después de estar desconectado), el número de solicitudes simultáneas puede exceder los límites del servidor. Un semáforo con un valor inicial de 3 garantiza un envío fluido y previene el bloqueo del lado del servidor.
Preguntas frecuentes
Semaphore no es solo un contador, sino un primitivo de sincronización con operaciones atómicas y una cola de espera. Un contador normal no bloquea un hilo ni garantiza la atomicidad del incremento cuando varios hilos acceden concurrentemente.
Sí, el deadlock es posible al adquirir múltiples semáforos en diferente orden por diferentes hilos. Por ejemplo, el hilo A adquiere S1, luego S2, mientras que el hilo B adquiere S2, luego S1. Fije un orden único de adquisición para todos los semáforos en el proyecto.
El hilo se bloquea y entra en estado de espera. No consume tiempo de CPU hasta que otro hilo llama a release. En Java, este es el estado BLOCKED; en Swift, el hilo es suspendido por GCD.
La principal diferencia es la propiedad. Un Mutex solo puede ser liberado por el hilo propietario. Un Binary Semaphore puede ser liberado por cualquier hilo, lo que es conveniente para la señalización entre hilos pero menos seguro para proteger la integridad de los datos.
El valor inicial depende del escenario. Para proteger un solo recurso — 1. Para un grupo de N conexiones — N. Para señalización entre hilos, use 0 para que el hilo consumidor espere una señal del productor.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también