- Aprender
- Informática
- Ciencias de la computación
- Sistemas operativos
- Semáforos e interbloqueo
Variables de condición, semáforos e interbloqueo
IntermedioUnidad 6 de 9 del curso Curso de Sistemas operativos
Lo que aprenderás
- Haz que un hilo se suspenda hasta que se cumpla una condición sobre datos compartidos, mediante una variable de condición junto con un cerrojo y una variable de estado, y usa este patrón para construir un búfer acotado correcto.
- Espera a que se cumpla una condición sin realizar espera activaExplica por qué un cerrojo por sí solo no puede hacer que un hilo espere un evento y usa espera y notificación con un cerrojo y una variable de estado para que no se pierda ningún despertar.
- Vuelve a comprobar las condiciones y elige entre notificar a un hilo o a todosAplica la semántica de Mesa comprobando de nuevo la condición en un bucle while, decide entre notificar a un hilo o a todos y reconoce las mismas reglas en los monitores y en Java.
- Resuelve el problema del productor-consumidor con variables de condiciónConstruye un búfer acotado compartido por hilos productores y consumidores, sigue por qué fallan las versiones ingenuas y corrígelas con bucles while y dos variables de condición.
- Espera a que se cumpla una condición sin realizar espera activa
- Usa el semáforo, una sola primitiva, como cerrojo, como señal entre hilos y como contador de recursos, y aplícalo al búfer acotado y a un cerrojo de lectores-escritores.
- Define un semáforo y úsalo como cerrojoDescribe cómo cambia el valor de un semáforo con las operaciones de espera y señalización, elige su valor inicial y usa un semáforo binario para la exclusión mutua.
- Ordena los hilos y limita la concurrencia mediante semáforosUsa un semáforo que comienza en cero para que un hilo espere a otro y un semáforo contador para limitar cuántos hilos usan un recurso a la vez.
- Construye un búfer acotado con semáforosResuelve el problema del productor-consumidor con dos semáforos contadores y un mutex, y explica por qué el orden en que se adquieren determina si el búfer funciona o se produce un interbloqueo.
- Comparte datos con un cerrojo de lectores-escritoresPermite que haya muchos lectores o un escritor a la vez, construye un cerrojo de este tipo a partir de semáforos y explica su problema de inanición y sus costes.
- Define un semáforo y úsalo como cerrojo
- Identifica las violaciones de atomicidad y del orden de ejecución, los dos patrones responsables de casi todos los errores de concurrencia distintos del interbloqueo encontrados en un estudio de software real, y corrige cada uno con la herramienta adecuada.
- Detecta y corrige violaciones de atomicidadReconoce el código que supone que un grupo de accesos se ejecuta sin interrupción, muestra el intercalado que lo estropea y protege todo el grupo con un solo cerrojo.
- Detecta y corrige violaciones del orden de ejecuciónReconoce el código que supone que un acceso ocurre antes que otro y garantiza ese orden con una variable de condición, un semáforo o la espera a la terminación de un hilo, en vez de depender de los tiempos.
- Detecta y corrige violaciones de atomicidad
- Explica cómo surge el interbloqueo a partir de cuatro condiciones, prevénlo eliminando una de ellas en el diseño y describe cómo los sistemas evitan interbloqueos con conocimiento previo o los detectan y se recuperan de ellos.
- Explica cómo se produce un interbloqueoDefine el interbloqueo, muestra el intercalado que lo causa, nombra las cuatro condiciones necesarias y usa un grafo de espera para decidir si los hilos están interbloqueados.
- Prevén los interbloqueos eliminando una condición necesariaDiseña el código para que una de las cuatro condiciones nunca pueda cumplirse: ordena la adquisición de cerrojos, adquiere todos los cerrojos a la vez, espera antes de reintentar con trylock o evita los cerrojos mediante instrucciones atómicas.
- Evita interbloqueos mediante la planificación y el algoritmo del banqueroUsa el conocimiento previo de lo que los hilos pueden solicitar para conceder solo las solicitudes que mantengan el sistema en un estado seguro, y aplica el algoritmo del banquero con números reales.
- Detecta interbloqueos y recupera el funcionamiento del sistemaPermite que ocurran interbloqueos, encuéntralos buscando ciclos, recupera el funcionamiento terminando un hilo o revirtiendo su estado, y compara esta estrategia con la prevención, la evitación y no hacer nada.
- Explica cómo se produce un interbloqueo
- Estructura un programa en torno a un bucle de eventos que reacciona a la disponibilidad de entrada y salida, evita el bloqueo mediante entrada/salida asíncrona y conservación del estado, y compara eventos e hilos.
- Ejecuta un bucle de eventos con select o pollEscribe un bucle de eventos de un solo hilo que pregunte al sistema operativo qué descriptores están disponibles, gestione cada evento hasta terminar y nunca se bloquee.
- Evita el bloqueo mediante entrada/salida asíncrona y conservación del estadoInicia operaciones de entrada/salida sin esperarlas, averigua cuándo han terminado y conserva durante ese intervalo el estado que necesita un manejador.
- Elige entre eventos e hilosCompara los diseños basados en eventos y en hilos en cuanto a control, memoria, uso de varios núcleos y facilidad de mantenimiento, y reconoce los errores que persisten en el código basado en eventos.
- Ejecuta un bucle de eventos con select o poll
Guía de estudio de Semáforos e interbloqueo
1Espera a que cambie el estado compartido
Un hilo suele necesitar algo más que acceso exclusivo: necesita que otro hilo termine un trabajo o ponga datos a su disposición. Las variables de condición le permiten suspenderse durante esa espera. Su funcionamiento correcto depende de una variable de estado compartida, un mutex y una transición cuidadosa entre la comprobación y la suspensión. Comienza con un padre que espera a un hijo y luego usa el mismo patrón para coordinar productores y consumidores sin desperdiciar tiempo de CPU ni perder despertares.
1.1 Estado, un mutex y un lugar donde suspenderse
Un mutex responde a «¿Quién puede acceder a estos datos ahora?». No responde a «¿Ha terminado el trabajo?». Para esperar un evento, un hilo necesita un registro del evento y una forma de suspenderse hasta que ese registro cambie.
Por qué la espera activa es ineficiente
El bucle while (done == 0) ; consume tiempo de CPU al comprobar repetidamente un indicador. En una sola CPU, ese trabajo retrasa al hijo que debe establecer el indicador. En C, una lectura sin sincronización de una variable ordinaria mientras otro hilo escribe en ella también es una carrera de datos. El compilador no está obligado a volver a cargar el indicador en cada iteración.
Una variable de condición proporciona a los hilos una cola de espera. POSIX proporciona pthread_cond_t, inicializado con PTHREAD_COND_INITIALIZER o pthread_cond_init(). La cola es independiente del estado que se comprueba: done registra la finalización; la variable de condición proporciona un lugar donde suspenderse.
Mantén juntas la comprobación y la espera
pthread_cond_wait(&c, &m) requiere que el hilo que llama tenga adquirido el mutex m. Libera ese mutex e inicia la espera de forma atómica. Antes de retornar, vuelve a adquirir el mutex. Esto cierra el peligroso intervalo en el que otro hilo podría cambiar el estado y notificar antes de que el hilo en espera haya empezado realmente a esperar.
Aquí un padre espera a que termine un hijo. Los nombres lock, wait, signal y unlock abrevian las llamadas correspondientes de la biblioteca de hilos.
/* Parent */
lock(&m);
while (done == 0)
wait(&c, &m);
unlock(&m);
/* Child */
lock(&m);
done = 1;
signal(&c);
unlock(&m);
Inicialmente done es 0. Si el padre se ejecuta primero, adquiere el cerrojo, comprueba que el trabajo no ha terminado y espera. La espera libera m, lo que permite al hijo adquirirlo, establecer done y notificar. Con el tiempo, el padre vuelve a adquirir m, comprueba de nuevo done y continúa.
Si el hijo se ejecuta primero, registra la finalización antes de que llegue el padre. Su notificación no despierta a nadie, pero el padre, al llegar después, ve done == 1 y omite la espera. El funcionamiento debe ser correcto en ambos órdenes.
Las notificaciones no almacenan eventos
pthread_cond_signal(&c) desbloquea al menos a un hilo en espera cuando los hay; POSIX permite que se despierte más de uno. Si no hay hilos en espera, no tiene efecto. pthread_cond_broadcast(&c) despierta a todos los hilos que esperan en ese momento. Ninguna de las dos llamadas guarda una notificación para futuros hilos.
Por tanto, omitir done estropea el patrón de espera a la terminación: un hijo puede notificar antes de que su padre espere, tras lo cual el padre se suspende sin que vaya a llegar otra notificación. Esto es un despertar perdido. Comprobar done fuera del mutex crea otra ventana de despertar perdido: el padre comprueba que es falso, el hijo lo establece como verdadero y notifica, y luego el padre empieza a esperar.
Protege la comprobación y cada cambio de estado con el mismo mutex. Mantenlo adquirido desde la comprobación hasta la llamada de espera. Una forma sencilla de notificar consiste en cambiar el estado y notificar mientras se sigue teniendo adquirido ese mutex. POSIX permite notificar sin tenerlo adquirido, pero el protocolo de estado compartido debe seguir evitando los despertares perdidos.
Puntos clave
- El estado registra el evento; la variable de condición proporciona una cola de espera.
- Llama a la espera con el mutex adquirido; la espera lo libera de forma atómica y lo vuelve a adquirir antes de retornar.
- Protege la comprobación y el cambio de estado con el mismo mutex.
- Una notificación enviada cuando no hay hilos en espera no se guarda.
1.2 Vuelve a comprobar la condición al despertar
Retornar de la espera significa que el hilo puede volver a intentarlo. No significa que la condición que necesita siga siendo verdadera. Esta distinción importa cuando varios hilos compiten por los mismos datos.
Semánticas de Mesa y Hoare
| Semántica | Qué proporciona una notificación al hilo en espera |
|---|---|
| Mesa | Disponibilidad para ejecutarse; el hilo en espera debe volver a adquirir el mutex y comprobar el estado. |
| Hoare | Entrega inmediata del cerrojo y de la ejecución, que preserva la condición notificada en ese momento. |
POSIX, Java, las variables de condición de C++ y las condiciones de Python siguen el comportamiento de Mesa. Los monitores de Hoare introdujeron el modelo de entrega más fuerte; el enfoque de Mesa es la opción práctica habitual.
Supón que C1 espera a que un búfer de una posición deje de estar vacío. Un productor lo llena y notifica a C1. C1 está ahora listo, pero C2 puede adquirir primero el mutex y tomar el elemento. Cuando C1 finalmente vuelve a adquirir el mutex, el búfer está vacío de nuevo.
lock(&m);
while (count == 0)
wait(&filled, &m);
v = get();
unlock(&m);
El while repite la comprobación después de cada retorno. Un if solo comprueba antes de la primera espera y permitiría que C1 llamara a get() sobre un búfer vacío. Incluso con un productor y un consumidor, usa while: POSIX también permite despertares espurios, en los que la espera retorna sin una notificación correspondiente.
¿Despertar a un hilo o a todos?
Una notificación a un hilo es adecuada cuando todos los hilos en espera necesitan la misma condición y cualquiera puede aprovechar el cambio. Una notificación a todos es útil cuando varios pueden continuar o cuando distintos hilos en espera necesitan cantidades diferentes y el hilo que notifica no puede seleccionar al adecuado.
Considera un asignador con 0 bytes libres. Ta necesita 100 bytes y Tb necesita 10. Otro hilo libera 50 bytes. Una notificación podría despertar a Ta, que aún no puede continuar, dejando a Tb suspendido aunque su solicitud puede atenderse. Notificar a todos despierta a ambos. Vuelven a adquirir el mutex de uno en uno; Tb puede asignar memoria, mientras Ta vuelve a esperar. Esta es una condición de cobertura: la notificación cubre a todos los hilos en espera que podrían continuar.
Notificar a todos puede causar despertares y cambios de contexto adicionales, y competencia por el mutex. Variables de condición separadas para distintos predicados pueden hacer que las notificaciones sean más selectivas. En un búfer, los productores y consumidores no deberían compartir una sola cola de notificación indiferenciada.
La relación con los monitores
Un monitor agrupa datos compartidos con operaciones que admiten un hilo a la vez y variables de condición para esperar. Los objetos de Java proporcionan un cerrojo y un conjunto de espera. Dentro de synchronized, un hilo puede llamar a wait(), notify() o notifyAll(); llamarlos sin ser propietario del monitor de ese objeto lanza IllegalMonitorStateException.
La espera solo libera el mutex asociado o el cerrojo del objeto Java. Si un hilo también tiene adquirido otro cerrojo, ese segundo cerrojo permanece adquirido. Un productor que lo necesite podría no poder hacer que la condición sea verdadera. Comprueba todos los cerrojos adquiridos al esperar, no solo el indicado en la llamada.
Puntos clave
- Según la semántica de Mesa, despertar deja listo a un hilo; este aún debe competir por el mutex.
- Espera en un bucle while para gestionar los cambios de estado y los despertares espurios.
- Notifica a todos cuando una sola notificación podría dejar suspendido a un hilo que puede continuar.
- Las distintas condiciones de espera pueden usar variables de condición separadas.
- La espera solo libera su cerrojo asociado.
1.3 Construye un búfer acotado con dos colas de espera
Los productores añaden elementos a un búfer de tamaño fijo; los consumidores los extraen. Un búfer acotado correcto impide sobrescribir un búfer lleno, extraer de uno vacío y modificar sus datos de control simultáneamente. La cola de solicitudes de un servidor web y la tubería entre grep error log.txt y wc -l siguen este patrón.
Sigue los índices circulares
Un búfer circular usa un arreglo y dos índices. fill_ptr indica la siguiente escritura; use_ptr indica la siguiente lectura. Ambos avanzan módulo MAX. Un count separado distingue el estado vacío del lleno, incluso cuando los dos índices son iguales.
void put(int v) {
buffer[fill_ptr] = v;
fill_ptr = (fill_ptr + 1) % MAX;
count++;
}
int get(void) {
int v = buffer[use_ptr];
use_ptr = (use_ptr + 1) % MAX;
count--;
return v;
}
Comienza con capacidad 3 y ambos índices en 0. Al insertar 7, 8 y 9 se llenan las posiciones 0, 1 y 2. El índice de escritura vuelve a 0 y count llega a 3. La extracción devuelve 7 y hace avanzar el índice de lectura a 1. Al insertar 10 se reutiliza la posición 0. El arreglo es ahora [10, 8, 9], pero la extracción FIFO sigue el índice de lectura: primero sale 8, luego 9 y luego 10.
Dirige cada notificación al papel opuesto
Usa un mutex, empty para los productores que esperan espacio y fill para los consumidores que esperan un elemento. Inicializa ambas variables de condición y el mutex antes de iniciar los hilos.
/* Producer: repeat for each item v */
lock(&m);
while (count == MAX)
wait(&empty, &m);
put(v);
signal(&fill);
unlock(&m);
/* Consumer */
lock(&m);
while (count == 0)
wait(&fill, &m);
v = get();
signal(&empty);
unlock(&m);
La comprobación y la actualización del búfer permanecen en una misma sección crítica. De lo contrario, dos productores podrían ver una posición libre y sobrescribir el trabajo del otro. La espera libera m, de modo que el papel opuesto puede cambiar el búfer.
Por qué una sola cola puede detenerse
Con una variable de condición y notificaciones a un hilo, los productores y consumidores pueden despertar al papel equivocado. Con capacidad 1, C1 y C2 primero se suspenden porque está vacío. P inserta un elemento, despierta a C1 y luego espera porque el búfer está lleno. C1 extrae el elemento y notifica, pero la notificación puede despertar a C2 en vez de a P. C1 vuelve a esperar; C2 también encuentra el búfer vacío y espera. P sigue suspendido aunque hay espacio disponible. Dos colas dirigen cada notificación al papel que ahora puede continuar. Notificar a todos en una cola compartida puede evitar ese problema de selección, a costa de despertares adicionales.
Una capacidad mayor absorbe ráfagas y permite solapar el trabajo de producción y consumo fuera de la breve sección crítica del búfer. Las actualizaciones del propio búfer siguen realizándose en serie. Una capacidad de 1 obliga a alternar cada inserción y extracción.
Para cerrar de forma ordenada, espera primero a que todos los productores terminen de colocar sus elementos reales. Luego pon en cola un elemento reservado de fin de datos por cada consumidor, como -1 cuando los elementos reales no son negativos. Cada consumidor sale después de tomar uno. Enviar esos marcadores mientras los productores siguen activos puede dejar elementos reales sin ningún consumidor.
Puntos clave
- Avanza los índices circulares módulo la capacidad; count distingue el estado vacío del lleno.
- Los productores esperan espacio y notifican que hay elementos; los consumidores esperan elementos y notifican que hay espacio.
- Usa bucles while y un mutex para proteger el predicado y la actualización del búfer.
- Una sola cola de notificación compartida puede despertar al papel equivocado y dejar suspendidos a todos los hilos.
- Espera a que terminen todos los productores antes de poner en cola un marcador de fin por cada consumidor.
- El orden FIFO sigue el índice de lectura, no el orden físico del arreglo.
2Cuenta permisos con semáforos
Un semáforo registra permisos que los hilos adquieren y devuelven. Su valor inicial permite que la misma primitiva garantice el acceso exclusivo, recuerde un evento o limite un conjunto de recursos. Sigue su valor a través de las operaciones de espera y señalización, y luego combina recuentos de permisos con un mutex para construir un búfer acotado. Un diseño de lectores-escritores añade otro uso: compartir el acceso entre lectores mientras se mantiene el acceso exclusivo para los escritores, con un coste de equidad que examinar.
2.1 Elige el valor inicial de un semáforo
Un semáforo contiene un entero que se modifica mediante operaciones atómicas de espera y señalización. Dijkstra lo introdujo en la década de 1960 como P y V; POSIX denomina a las operaciones sem_wait() y sem_post(). Atómico significa que dos hilos no pueden realizar parcialmente la misma operación sobre el contador a la vez.
Las unidades disponibles determinan el valor inicial
sem_init(&s, 0, X) crea un semáforo sin nombre con valor X. El segundo argumento 0 selecciona la compartición entre hilos de un mismo proceso. Para compartirlo entre procesos, un argumento distinto de cero requiere que el semáforo esté en memoria compartida.
| Propósito | Valor inicial | Significado |
|---|---|---|
| Cerrojo | 1 | Un hilo puede entrar inmediatamente. |
| Ordenación de eventos | 0 | El evento no ha ocurrido. |
| Conjunto de recursos | N | Se pueden adquirir N unidades ahora. |
Un semáforo binario usado como cerrojo rodea una sección crítica con una espera y una señalización. Iniciarlo en 0 bloquea al primer hilo que intenta entrar. Iniciarlo en 2 permite la entrada de dos hilos y no proporciona exclusión mutua.
Dos modelos de recuento equivalentes
En la definición de Dijkstra, el valor permanece no negativo: la espera suspende el hilo hasta que puede decrementar sin bajar de cero, y la señalización añade una unidad. Linux sigue esta convención de valores no negativos.
Para los seguimientos manuales, usa el modelo de contador con signo: la espera decrementa y bloquea si el nuevo valor es negativo; la señalización incrementa y despierta a un hilo en espera cuando hay uno bloqueado. En este modelo, la magnitud de un valor negativo cuenta los hilos bloqueados. Es una convención para razonar, no un valor negativo que debas esperar de la API de semáforos de Linux.
Sigue un cerrojo inicializado en 1:
- T0 espera: 1 pasa a 0, por lo que T0 entra.
- T1 espera: 0 pasa a −1, por lo que T1 se bloquea.
- T0 señaliza: −1 pasa a 0, lo que permite a T1 continuar.
- T1 termina y señaliza: 0 pasa a 1.
El despertar transfiere el permiso existente a T1; no se necesita otra espera antes de que T1 entre en su sección crítica.
Una señalización se recuerda
Si no hay ningún hilo en espera, una señalización aumenta el recuento disponible. Una espera futura puede consumir ese permiso. Esto es útil cuando un hilo termina antes de que el hilo que lo espera haya empezado a esperar. Una notificación de variable de condición no tiene esa memoria.
Los semáforos no tienen propietario: un hilo puede esperar y otro puede señalizar. Un mutex pertenece al hilo que lo adquirió; liberar un mutex POSIX predeterminado desde otro hilo es un comportamiento indefinido. El semáforo sin propietario permite la señalización, pero también dificulta el diagnóstico de una señalización adicional.
Un semáforo puede construirse con un mutex, una variable de condición y un entero no negativo. La espera adquiere el mutex, entra en un bucle mientras value <= 0, espera, decrementa y libera el mutex. La señalización adquiere el mutex, incrementa, notifica y libera el mutex. El mutex hace atómicas las operaciones del contador; el bucle gestiona los despertares que no dejan una unidad disponible. Los cerrojos con variables de condición y los semáforos pueden implementarse unos con otros, aunque implementar correctamente una variable de condición a partir de semáforos es más complejo.
Puntos clave
- Inicializa un semáforo con el número de unidades disponibles de inmediato.
- Un semáforo usado como cerrojo comienza en 1 y combina una espera antes de la sección con una señalización después.
- El modelo de seguimiento con contador con signo usa valores negativos para los hilos bloqueados; los valores de Linux permanecen no negativos.
- Las señalizaciones se recuerdan como permisos, incluso cuando no hay hilos en espera.
- Un semáforo no tiene propietario; un mutex sí.
2.2 Recuerda eventos y limita el uso de recursos
El mismo par de espera y señalización puede representar un evento o una cantidad limitada de recursos. Decide qué significa un permiso antes de elegir el recuento inicial y dónde corresponde cada operación.
Haz que la finalización preceda al uso
Para que un padre espere a un hijo, inicializa finished en 0. El hijo señaliza después de terminar su trabajo. El padre espera antes de usar el resultado.
/* Child */
produce_result();
sem_post(&finished);
/* Parent */
sem_wait(&finished);
use_result();
Si el padre llega primero, el valor con signo cambia de 0 a −1 y se suspende. La señalización del hijo devuelve el valor a 0 y lo despierta. Si el hijo llega primero, su señalización cambia el valor de 0 a 1; la espera posterior lo cambia de 1 a 0 sin bloquearse. Un evento completado permite una espera satisfactoria.
Cuenta un conjunto de recursos
Un conjunto de cuatro conexiones a una base de datos usa un semáforo inicializado en 4. Cada cliente espera antes de tomar prestada una conexión y señaliza después de devolverla. Un quinto cliente simultáneo se suspende. El semáforo cuenta las admisiones; un mutex independiente sigue protegiendo la lista compartida que identifica qué conexión está libre. Dos hilos admitidos no deben retirar la misma entrada de la lista.
La limitación de la concurrencia aplica la misma idea a una fase costosa. Supón que 100 hilos necesitan cada uno 500 MB durante el procesamiento. Juntos podrían necesitar 50 000 MB. Una máquina con 8 GB tiene 8000 MB según las unidades decimales del SI. Admitir como máximo 12 trabajadores limita esta fase a 6000 MB y deja 2000 MB para otros usos.
sem_wait(&processing_slots);
run_memory_heavy_phase();
sem_post(&processing_slots);
Toda ruta posterior a una espera satisfactoria debe devolver su permiso, incluida una ruta de error. Olvidar una señalización reduce gradualmente la capacidad. Señalizar dos veces crea un permiso adicional y permite más usuarios simultáneos de los previstos.
Reunirse en un punto de encuentro
Un punto de encuentro requiere que lleguen dos hilos antes de que cualquiera continúe. Inicializa a_arrived y b_arrived en 0.
/* A */
sem_post(&a_arrived);
sem_wait(&b_arrived);
/* B */
sem_post(&b_arrived);
sem_wait(&a_arrived);
Si A llega primero, registra su llegada y se bloquea esperando a B. B registra su llegada y puede consumir el permiso guardado de A. A puede entonces consumir el permiso de B. Invertir ambas secuencias para esperar antes de señalizar deja a cada hilo suspendido antes de anunciar nada.
Estos dos semáforos implementan un encuentro. Reutilizar la sincronización en rondas repetidas requiere evitar que unas rondas consuman los anuncios de llegada de otras; un seguimiento satisfactorio no constituye un diseño de barrera reutilizable.
Un semáforo se adapta bien a un recuento o a un evento sencillo. Una variable de condición se adapta a un predicado más complejo, como esperar hasta que una cola contenga varios elementos de un tipo determinado. En ese caso, el mutex protege el estado a partir del cual se calcula la condición.
Puntos clave
- Inicia un semáforo de ordenación en 0 y señaliza solo después del evento que representa.
- Un semáforo contador limita la admisión, pero no protege la lista de recursos.
- La limitación de la concurrencia acota la demanda simultánea de memoria u otro recurso limitado.
- En un punto de encuentro de dos hilos, anuncia la llegada antes de esperar al otro hilo.
- Una señalización adicional crea capacidad; una señalización omitida reduce la capacidad.
2.3 Reserva una posición antes de adquirir el cerrojo del búfer
Un búfer con semáforos separa dos preguntas: «¿Hay un elemento o una posición disponible?» y «¿Quién puede actualizar el arreglo ahora?». Dos semáforos contadores responden a la primera pregunta y un mutex responde a la segunda.
Prepara tres controles distintos
Para una capacidad MAX, inicializa empty en MAX, full en 0 y el mutex como liberado. Aquí empty y full son semáforos, no variables de condición. Un permiso de empty reserva una posición; un permiso de full reserva un elemento.
/* Producer */
sem_wait(&empty);
lock(&mutex);
put(v);
unlock(&mutex);
sem_post(&full);
/* Consumer */
sem_wait(&full);
lock(&mutex);
v = get();
unlock(&mutex);
sem_post(&empty);
El productor adquiere espacio antes de acceder al búfer circular y luego publica el elemento terminado mediante una señalización de full. El consumidor adquiere un elemento, lo extrae con el mutex adquirido y devuelve el espacio mediante una señalización de empty. Una espera satisfactoria en un semáforo ya reserva el recurso, por lo que estas llamadas no necesitan un bucle de comprobación del predicado como el de una variable de condición.
Sigue una posición
Inicialmente empty vale 1 y full vale 0. Un consumidor espera en full, cuyo valor pasa a −1 en el modelo con signo, y se suspende. El productor espera en empty, cuyo valor pasa de 1 a 0. Adquiere el mutex, inserta, lo libera y señaliza full, cuyo valor pasa de −1 a 0 y permite continuar al consumidor.
La siguiente espera del productor en empty cambia el valor de 0 a −1 y lo bloquea. El consumidor toma el elemento con el mutex adquirido y señaliza empty, cuyo valor pasa de −1 a 0 y permite continuar al productor. Los recuentos de los semáforos realizan la transferencia; no se requiere un count separado para la sincronización.
Con un búfer más grande, varios productores pueden pasar el semáforo de espacio. Sin el mutex, ambos podrían leer el mismo fill_ptr y escribir en la misma posición. Una reserva no concede acceso exclusivo a los índices del arreglo.
Por qué adquirir primero el mutex puede causar un interbloqueo
Supón que el consumidor adquiere mutex antes de esperar en full. Con el búfer vacío se suspende mientras sigue teniendo adquirido el mutex. El productor necesita ese mutex para insertar el elemento cuya señalización permitiría continuar al consumidor. Ninguno puede terminar.
Una espera en un semáforo no libera ningún mutex. Una espera en una variable de condición libera deliberadamente su mutex asociado. Por tanto, el orden en este búfer importa: primero reserva, después adquiere el mutex, cambia el búfer, libera el mutex y luego señaliza el permiso opuesto.
La regla general es evitar bloquearse mientras se tiene adquirido un cerrojo que necesita la operación capaz de desbloquear al hilo. Esta regla explica por qué el algoritmo de lectores-escritores puede tener una disposición segura diferente: su escritor no necesita el cerrojo del contador de lectores para liberar su recurso.
El propio mutex puede ser un semáforo binario inicializado en 1. Su función sigue siendo la exclusión mutua; los otros dos semáforos cuentan recursos.
Puntos clave
- Inicializa empty con la capacidad, full en 0 y el mutex como liberado.
- Reserva espacio o un elemento antes de adquirir el mutex del búfer.
- Protege los índices circulares y las actualizaciones del arreglo incluso cuando los permisos se cuentan correctamente.
- Esperar en un semáforo no libera ningún mutex.
- Señaliza full después de insertar; señaliza empty después de extraer.
- Un hilo bloqueado no debe tener adquirido un cerrojo que necesita el hilo capaz de desbloquearlo.
2.4 Permite que los lectores compartan el acceso y los escritores trabajen solos
Las estructuras en las que predominan las lecturas pueden permitir varios lectores a la vez porque leer no cambia los datos. Un cerrojo de lectores-escritores admite a un grupo de lectores o a un escritor. Todo cambio en los datos protegidos requiere acceso de escritura.
Construye el grupo de lectores
Usa readers = 0, un semáforo lock inicializado en 1 para proteger ese contador y writelock inicializado en 1 para proteger el recurso compartido.
/* Acquire read access */
sem_wait(&lock);
readers++;
if (readers == 1)
sem_wait(&writelock);
sem_post(&lock);
/* Release read access */
sem_wait(&lock);
readers--;
if (readers == 0)
sem_post(&writelock);
sem_post(&lock);
Un escritor espera en writelock, cambia los datos y luego señaliza writelock. No adquiere el cerrojo del contador. El primer lector adquiere writelock para el grupo y el último lector lo devuelve. Un semáforo es adecuado para esa transferencia porque el hilo que lo señaliza no tiene que ser el que esperó originalmente.
Sigue la entrada de R1: aumenta readers a 1 y adquiere writelock. R2 aumenta el contador a 2 sin adquirir otro permiso. El escritor W se bloquea entonces en writelock. R1 sale y reduce el contador a 1. R2 sale, lo reduce a 0 y señaliza, lo que permite a W escribir en solitario.
El primer lector puede esperar a writelock mientras tiene adquirido lock. Esto no crea el interbloqueo del búfer acotado: el escritor actual no necesita el cerrojo del contador para terminar y señalizar writelock.
La equidad es una propiedad independiente
Este diseño da preferencia a los lectores. Mientras el contador de lectores siga siendo positivo, otro lector puede unirse al grupo antes que un escritor en espera. Un flujo continuo de lectores cuyas lecturas se solapan puede mantener al escritor esperando indefinidamente: inanición de los escritores.
| Política | Elección de admisión | Riesgo |
|---|---|---|
| Preferencia a los lectores | Los nuevos lectores pueden unirse a los lectores activos. | Los escritores pueden sufrir inanición. |
| Preferencia a los escritores | Los escritores en espera impiden la entrada de nuevos lectores. | Los lectores pueden sufrir inanición. |
| Admisión equitativa | Los hilos que llegan reciben acceso en un orden equitativo. | Más trabajo de gestión de la admisión. |
El diseño básico proporciona seguridad, pero no establece un límite al tiempo de espera de un escritor. Una mayor tasa de procesamiento de lectores no equivale a una atención equitativa.
Usa el modo correcto
Un cerrojo de lectura no puede proteger una escritura frente a otros lectores. Actualizar incluso un contador de caché con acceso de lectura puede entrar en carrera con otro lector. Para convertir el acceso en este diseño básico, libera el acceso de lectura, adquiere el de escritura y vuelve a comprobar la condición. Esperar el acceso de escritura mientras el hilo sigue contado como lector puede hacer que se espere a sí mismo para siempre.
POSIX proporciona pthread_rwlock_t, pthread_rwlock_rdlock(), pthread_rwlock_wrlock() y pthread_rwlock_unlock(). Java ofrece ReentrantReadWriteLock; C++17 ofrece std::shared_mutex. Estos cerrojos requieren más trabajo de gestión que un mutex sencillo. Mide la carga de trabajo: las secciones cortas o las escrituras frecuentes pueden hacer que un mutex sencillo sea igual de rápido o más rápido.
Puntos clave
- Un cerrojo de lectores-escritores permite muchos lectores o un escritor con acceso exclusivo.
- El primer lector adquiere el permiso del grupo; el último lector lo devuelve.
- El contador de lectores necesita su propio cerrojo.
- La preferencia a los lectores puede causar inanición a un escritor en espera.
- Libera el acceso de lectura antes de solicitar el acceso de escritura y luego vuelve a comprobar los datos.
3Encuentra la suposición sobre el orden que causa un error
Muchos fallos de concurrencia permiten que el programa siga ejecutándose con datos incorrectos. Dos causas recurrentes son tratar varios accesos como una acción indivisible y suponer que el trabajo de un hilo precede al de otro. Seguir unos pocos accesos elegidos con cuidado suele revelar la suposición oculta. La solución depende de esa suposición: protege una operación completa cuando deba mantenerse unida y establece una dependencia explícita cuando una operación deba ocurrir primero.
3.1 Protege toda la comprobación y la acción
Una violación de atomicidad ocurre cuando el código depende de que un grupo de accesos sea indivisible, pero otro hilo puede actuar entre ellos. Cada acceso individual puede parecer razonable mientras la operación conjunta falla.
Encuentra el intervalo después de la comprobación
Considera un búfer de registro compartido:
/* T1 */
if (log->buf != NULL)
write_out(log->buf);
/* T2 */
log->buf = NULL;
T1 puede comprobar un puntero no nulo, pausarse y reanudar su ejecución después de que T2 lo establezca como nulo. Entonces T1 pasa un valor nulo a write_out(). La garantía buscada no era solo una comprobación válida; también era que el valor comprobado siguiera siendo utilizable durante la llamada.
Protege la comprobación y el uso juntos, y protege la asignación que compite con ellos con el mismo mutex:
/* T1 */
lock(&m);
if (log->buf != NULL)
write_out(log->buf);
unlock(&m);
/* T2 */
lock(&m);
log->buf = NULL;
unlock(&m);
Ahora T2 se ejecuta antes o después de toda la operación de T1. Dar a cada hilo su propio cerrojo no ayuda: ambos podrían tener adquiridos sus cerrojos independientes simultáneamente. Proteger solo a T1 deja a T2 libre para entrar en el intervalo.
Una comprobación válida aún puede dar lugar a un saldo no válido
Comienza con un saldo de 50. Dos hilos pretenden retirar cada uno 30 solo si queda suficiente. Ambos pueden leer 50 y superar balance >= 30 antes de que cualquiera actúe. Si se aplican las retiradas, el saldo puede llegar a −10. Proteger juntas la comprobación y la resta permite que la primera retirada deje 20; el siguiente hilo ve 20 y rechaza la retirada.
Incluso las cargas y restas atómicas no convierten ese par en una sola transacción. Otro hilo puede actuar entre la comprobación atómica y el cambio atómico. Un cerrojo alrededor de toda la decisión y el cambio garantiza el límite necesario.
El mismo problema aparece en count++: la carga, la suma y el almacenamiento son etapas separadas. Dos hilos pueden cargar el mismo valor anterior y almacenar el mismo valor incrementado, perdiendo una actualización. Un cerrojo alrededor del incremento o una operación atómica de obtener y sumar realiza la actualización necesaria de forma segura.
Usa las pruebas sin generalizar en exceso
Lu y sus colegas examinaron 105 errores de concurrencia corregidos en MySQL, Apache, Mozilla y OpenOffice en un estudio de 2008. De ellos, 74 eran errores distintos del interbloqueo y 31 eran interbloqueos. Las violaciones de atomicidad y del orden de ejecución representaban el 97 % de la muestra de errores distintos del interbloqueo. Estos son resultados de la muestra estudiada, no un porcentaje garantizado para todas las bases de código.
Alrededor del 92 % de los errores estudiados podían provocarse controlando el orden de como máximo cuatro accesos a memoria. Por tanto, una pequeña tabla de intercalados puede revelar más que miles de ejecuciones normales satisfactorias. ThreadSanitizer ayuda a encontrar carreras de datos ejecutadas, pero un error lógico de atomicidad puede persistir incluso cuando cada acceso individual está sincronizado. Comprueba la propiedad invariante de toda la operación, como «una retirada satisfactoria nunca deja el saldo en descubierto».
Puntos clave
- Delimita la sección crítica alrededor de toda la operación que debe ser indivisible.
- Todos los accesos que compiten deben seguir el mismo protocolo de cerrojos.
- Los accesos individuales atómicos aún pueden dejar un intervalo entre la comprobación y la acción.
- Sigue un pequeño intercalado perjudicial en vez de depender solo de ejecuciones satisfactorias repetidas.
3.2 Haz que una operación ocurra antes que otra
Una violación del orden de ejecución significa que la operación A debe ocurrir antes que la operación B, pero el programa no proporciona una sincronización que lo garantice. La exclusión mutua por sí sola no puede decidir qué operación va primero.
Un hilo puede ejecutarse de inmediato
Supón que un creador almacena un registro de trabajador recién creado en un puntero compartido, mientras el trabajador empieza usando ese puntero:
/* Creator */
worker = thread_create(worker_main);
/* New worker */
use(worker->state);
El trabajador puede ejecutarse antes de que retorne thread_create() y antes de que termine la asignación. El orden del código fuente del creador no dice nada sobre cuándo obtiene tiempo de CPU el nuevo hilo. Una ejecución satisfactoria solo significa que el creador ganó esa carrera por casualidad.
Publica la disponibilidad después de la inicialización
Añade ready = 0, el mutex m y la variable de condición c. El creador mantiene adquirido el mutex mientras asigna el puntero y publica que está disponible:
/* Creator */
lock(&m);
worker = thread_create(worker_main);
ready = 1;
signal(&c);
unlock(&m);
/* Worker, before using worker */
lock(&m);
while (ready == 0)
wait(&c, &m);
unlock(&m);
use(worker->state);
Si el trabajador llega primero al mutex, no puede superar la comprobación de disponibilidad hasta que el creador haya publicado el registro. Si llega más tarde, ready recuerda que terminó la inicialización. Mantén el registro válido mientras el trabajador lo usa y sincroniza cualquier cambio posterior.
Un semáforo inicializado en 0 ofrece una transferencia más breve: el creador señaliza después de asignar el puntero y el trabajador espera antes de usarlo. Para un resultado que solo existe después de que termina un hilo, esperar a que termine antes de leer su resultado establece el orden necesario. Del mismo modo, la liberación de un objeto debe esperar a que terminen quienes lo usan, por ejemplo mediante esperas a la terminación de los hilos o un contador de referencias correctamente sincronizado.
Por qué los cambios de tiempo no lo corrigen
Añadir sleep(1) o sched_yield() solo cambia la probabilidad de la planificación deseada. Un sistema más lento o cargado aún puede permitir que el consumidor continúe demasiado pronto. Un mutex alrededor de cada operación impide el solapamiento, pero sigue permitiendo que el uso adquiera el mutex antes de la inicialización.
| Suposición oculta | Tipo de error | Garantía necesaria |
|---|---|---|
| «Nada cambia entre esta comprobación y la acción». | Violación de atomicidad | Una operación protegida. |
| «La inicialización termina antes de este uso». | Violación del orden de ejecución | Una dependencia explícita de finalización o disponibilidad. |
En el estudio de Lu y sus colegas, alrededor de un tercio de los errores distintos del interbloqueo eran violaciones del orden de ejecución y alrededor del 73 % de los errores distintos del interbloqueo no se corrigieron simplemente añadiendo o cambiando cerrojos. Antes de elegir una primitiva, identifica la operación que debe terminar y la operación que debe esperar. Esa dependencia determina dónde corresponde la señalización, la notificación o la espera a la terminación.
Puntos clave
- Una violación del orden de ejecución supone que una acción anterior ha terminado sin garantizar ese orden.
- Publica la disponibilidad solo después de la inicialización y espera antes de usar el resultado.
- Un cerrojo impide el solapamiento, pero no determina quién va primero.
- La suspensión y la cesión de la CPU cambian los tiempos sin garantizar una dependencia.
- Mantén los objetos compartidos válidos hasta que terminen todos sus usuarios.
4Razona sobre el interbloqueo y el progreso
El interbloqueo convierte un conjunto de esperas ordinarias en una detención permanente: cada hilo necesita algo que tiene otro hilo bloqueado. Un grafo de dependencias hace visible ese ciclo. Las cuatro condiciones necesarias sugieren formas de prevenirlo; las demandas de recursos declaradas de antemano permiten evitarlo; y un detector puede encontrar un interbloqueo después de que se forme. Compara estas estrategias por el conocimiento que necesitan, la concurrencia que cuestan y el trabajo que puede perderse durante la recuperación.
4.1 Sigue el ciclo de hilos en espera
Un conjunto de hilos está interbloqueado cuando cada uno espera algo que solo otro hilo del conjunto puede proporcionar. Ninguno puede continuar y realizar la acción que libera a los demás. El programa puede funcionar con muchas planificaciones antes de que un orden desfavorable revele el ciclo.
Construye el fallo de dos cerrojos
T1 adquiere L1 y luego solicita L2. T2 adquiere L2 y luego solicita L1. Si T1 adquiere L1 y después T2 adquiere L2, ambas segundas adquisiciones se bloquean. Cada hilo tiene exactamente el recurso que necesita el otro.
Esto puede ocultarse dentro de funciones ordinarias. Si transfer(a, b) adquiere el cerrojo de a y luego el de b, las llamadas simultáneas transfer(x, y) y transfer(y, x) pueden adquirir los cerrojos de las cuentas en órdenes opuestos. La encapsulación facilita una llamada, pero puede ocultar los cerrojos que adquiere.
Identifica las cuatro condiciones
| Condición necesaria | Significado en el ejemplo de cerrojos |
|---|---|
| Exclusión mutua | Solo un hilo tiene adquirido un cerrojo determinado. |
| Retención y espera | Cada uno tiene adquirido un cerrojo mientras espera otro. |
| Ausencia de expropiación | Un cerrojo adquirido no se retira por la fuerza. |
| Espera circular | Cada uno espera el cerrojo que tiene el siguiente hilo. |
Las cuatro condiciones son necesarias. Eliminar cualquiera de ellas en el diseño previene el interbloqueo de recursos. La presencia de cerrojos exclusivos y adquisiciones anidadas por sí sola no demuestra que una ejecución concreta haya llegado a un ciclo.
Interpreta un grafo de espera
Dibuja un nodo por hilo. Dibuja T → U cuando T espera un recurso que tiene U. Con recursos de una sola instancia, como los cerrojos exclusivos, un ciclo identifica un interbloqueo.
Si T1 tiene A y espera B, T2 tiene B y espera C, y T3 tiene C y espera A, el grafo es T1 → T2 → T3 → T1. Los tres están detenidos. Si T3 solicita en cambio un cerrojo libre D, puede terminar y liberar C, permitiendo que T2 y luego T1 continúen. Una cadena no es un ciclo.
Cuando los tipos de recurso tienen varias instancias intercambiables, un ciclo por sí solo no basta: puede quedar disponible otra instancia. Usa los recuentos de recursos y las solicitudes pendientes para evaluar ese caso.
Los filósofos comensales y fallos relacionados
Cinco filósofos comparten cinco tenedores dispuestos en círculo. Cada uno necesita los dos tenedores contiguos. Si todos toman primero su tenedor izquierdo, todos pueden esperar su tenedor derecho para siempre. Invertir el orden de adquisición de un filósofo rompe el ciclo. Limitar la admisión a cuatro filósofos también impide que los cinco retengan un tenedor; al menos un filósofo admitido puede obtener ambos tenedores.
La inanición significa que un hilo espera indefinidamente mientras otros avanzan. El bloqueo activo significa que los hilos se ejecutan y reaccionan, pero no consiguen avanzar repetidamente. Un conjunto interbloqueado está bloqueado. Un hilo puede incluso interbloquearse a sí mismo al adquirir un mutex normal, no recursivo, que ya tiene adquirido. Los semáforos y las variables de condición también pueden crear dependencias: un hilo en espera que tiene adquirido un cerrojo necesario para quien debe notificarlo puede cerrar un ciclo sin adquirir explícitamente un segundo cerrojo.
Puntos clave
- El interbloqueo es un ciclo permanente de dependencias entre hilos bloqueados.
- Deben cumplirse las cuatro condiciones: exclusión mutua, retención y espera, ausencia de expropiación y espera circular.
- Para recursos de una sola instancia, un ciclo en el grafo de espera identifica un interbloqueo.
- Un ciclo con recursos de varias instancias requiere un análisis adicional.
- La inanición, el bloqueo activo y el interbloqueo describen distintos fallos de progreso.
- La adquisición de cerrojos oculta dentro de funciones puede invertir el orden de adquisición.
4.2 Elimina una condición que permite el interbloqueo
La prevención cambia el diseño para que no pueda surgir una condición necesaria del interbloqueo. El orden de adquisición de cerrojos suele ser el enfoque más claro porque ataca directamente la dependencia circular.
Asigna un orden a cada cerrojo
Asigna claves únicas a los cerrojos y adquiere siempre las claves menores antes que las mayores. Para las transferencias entre cuentas, ordena los identificadores de las cuentas antes de adquirir sus cerrojos. Así, tanto transfer(x, y) como transfer(y, x) adquieren primero el cerrojo de la misma cuenta. Si los identificadores designan la misma cuenta, adquiere su cerrojo una sola vez.
Un ciclo requeriría que las claves siguieran aumentando alrededor del círculo y volvieran a la clave inicial, lo cual es imposible. Todas las rutas de adquisición deben seguir la regla. Una sola función auxiliar que invierta el orden restablece el riesgo. lockdep de Linux registra las dependencias observadas entre cerrojos e informa de órdenes potencialmente inseguros.
Adquiere todo o no adquieras nada
Una forma estricta de eliminar la retención y espera es conceder a un hilo su conjunto completo de recursos en un solo paso, o no conceder nada. Así nunca retiene un conjunto parcial mientras espera el resto. Esto requiere conocer de antemano los recursos necesarios y puede reducir la concurrencia.
Un protocolo relacionado serializa las secuencias de adquisición con un cerrojo global de prevención:
lock(&prevention);
lock(&L1);
lock(&L2);
unlock(&prevention);
/* use L1 and L2, then release them */
Todos los hilos deben terminar de adquirir su conjunto completo bajo ese cerrojo global, y la liberación de recursos no debe requerir adquirirlo. Un hilo aún puede tener L1 mientras espera L2, por lo que esto no elimina literalmente la retención y espera. Impide que otros hilos comiencen una adquisición parcial que compita con ella. Ningún hilo que tenga recursos puede estar a mitad de una segunda secuencia de adquisición que compita con la primera. Los costes son adquirir los cerrojos antes y serializar la adquisición.
Renuncia y reintenta
pthread_mutex_trylock() retorna inmediatamente con EBUSY cuando un cerrojo no está disponible. Un hilo puede adquirir L1, intentar adquirir L2 y liberar L1 si el intento falla. También debe deshacer otras adquisiciones parciales antes de reintentar.
Dos hilos pueden colisionar repetidamente, esperar antes de reintentar y volver a intentarlo juntos. Eso es bloqueo activo: ambos se ejecutan, pero ninguno termina. La espera aleatoria antes del reintento reduce las colisiones repetidas; no convierte el bucle de reintento en una garantía de equidad.
Sustituye un cerrojo por una operación atómica
Comparar e intercambiar (CAS) cambia una ubicación solo si aún coincide con un valor esperado. Conceptualmente, un bucle de reintento de suma atómica lee el valor anterior, intenta sustituirlo por ese valor más el incremento y reintenta si otro hilo lo cambió primero. Las implementaciones reales en C usan tipos y operaciones atómicas tanto para la lectura como para la actualización.
Eliminar la adquisición de un cerrojo elimina el interbloqueo por cerrojos de esa operación. Un algoritmo sin cerrojos garantiza el progreso del sistema en su conjunto, pero un hilo puede seguir fallando en sus reintentos. Las estructuras generales sin cerrojos también necesitan un diseño cuidadoso del tiempo de vida de la memoria.
| Técnica | Principal contrapartida |
|---|---|
| Adquisición ordenada | Todas las rutas del código deben respetar el orden. |
| Adquisición simultánea | Los recursos deben conocerse y pueden quedar sin usar. |
| Trylock y reintento | Hay que atender al trabajo de reintento y al bloqueo activo. |
| Actualización sin cerrojos | El razonamiento sobre la corrección es más exigente. |
Puntos clave
- Un orden común de cerrojos previene la espera circular solo cuando todas las rutas lo siguen.
- La verdadera adquisición de todo o nada elimina la retención y espera.
- Un cerrojo global de adquisición serializa las secuencias completas de adquisición.
- Si falla trylock, libera los recursos adquiridos parcialmente antes de reintentar.
- La espera aleatoria antes del reintento reduce las colisiones que causan bloqueo activo.
- El progreso sin cerrojos del sistema no garantiza el progreso de cada hilo.
4.3 Comprueba si una solicitud de recursos es segura
La evitación comprueba si una solicitud propuesta deja una forma de que todos los hilos terminen. Permite los patrones habituales de uso de recursos, pero retrasa las solicitudes que eliminan los órdenes seguros de finalización restantes.
Un estado seguro garantiza que existe un orden posible
Una secuencia segura ordena los hilos de modo que cada uno pueda obtener su demanda máxima restante a partir de los recursos actualmente libres más los que liberan los hilos anteriores. Un estado inseguro carece de ese orden garantizado. No tiene por qué estar ya interbloqueado: los hilos podrían solicitar menos de su máximo o liberar recursos antes.
La planificación también puede evitar solapamientos peligrosos garantizando que dos tareas que entran en conflicto no accedan a sus fases de uso de recursos al mismo tiempo. No basta con colocar ambas en una CPU, porque aún pueden ser expropiadas e intercalarse mientras retienen recursos. La garantía necesaria es que las fases que entran en conflicto no se solapen.
Prepara las tablas del banquero
Para cada tipo de recurso, registra Available. Para cada hilo, registra Max, Allocation y Need = Max - Allocation. Compara los vectores componente por componente, nunca por la suma de sus componentes.
Usa los totales de recursos (7, 5, 6):
| Hilo | Allocation | Max | Need |
|---|---|---|---|
| T0 | (1, 1, 0) | (5, 3, 2) | (4, 2, 2) |
| T1 | (2, 0, 1) | (3, 2, 2) | (1, 2, 1) |
| T2 | (1, 1, 2) | (2, 2, 4) | (1, 1, 2) |
| T3 | (0, 1, 1) | (4, 2, 3) | (4, 1, 2) |
Las asignaciones suman (4, 3, 4), lo que deja Available = (3, 2, 2).
Sigue la comprobación de seguridad
Establece Work como Available. Elige un hilo que no haya terminado y cuyo Need quepa en Work, simula que termina y añade su Allocation de nuevo a Work.
- T1 cabe en (3, 2, 2). Devolver (2, 0, 1) da Work (5, 2, 3).
- T0 cabe. Devolver (1, 1, 0) da (6, 3, 3).
- T2 cabe. Devolver (1, 1, 2) da (7, 4, 5).
- T3 cabe. Devolver (0, 1, 1) da (7, 5, 6).
Todos los hilos terminan en esta simulación, por lo que el estado es seguro. Work aumenta en Allocation, no en Max: los recursos adicionales concedidos temporalmente para terminar también se devuelven, lo que da una ganancia neta igual a lo que el hilo tenía originalmente.
Simula la concesión y vuelve a comprobar
Una solicitud debe caber tanto en Need del hilo como en Available. Después réstala de Available, súmala a Allocation, réstala de Need y ejecuta la comprobación de seguridad sobre ese estado provisional.
Desde la tabla original, si T2 solicita (1, 0, 1), Available queda en (2, 2, 1). T1 aún puede ir primero, seguido de T0, T2 y T3, por lo que es seguro.
En cambio, si T3 solicita (2, 1, 1), Available queda en (1, 1, 1). El Need restante de T3 es (2, 0, 1). Ningún hilo cabe: T0 necesita 4 de A, T1 necesita 2 de B, T2 necesita 2 de C y T3 necesita 2 de A. Retrasa esta solicitud pese a los recursos actualmente libres.
Cada solicitud posterior necesita la misma comprobación. La evitación necesita demandas máximas declaradas y un modelo de recursos conocido, lo que la hace más adecuada para sistemas especializados y predecibles que para cargas de trabajo de aplicaciones generales.
Puntos clave
- Need es igual a la demanda máxima menos la asignación actual.
- Una secuencia segura debe satisfacer cada tipo de recurso por separado.
- Después de simular la finalización, añade Allocation a Work.
- Una solicitud debe caber en Need y en Available, y dejar un estado seguro.
- Inseguro significa que no hay una secuencia de finalización garantizada; no significa que ya se haya formado un interbloqueo.
4.4 Detecta un interbloqueo y elige cómo recuperarte
Un sistema puede permitir que se forme un interbloqueo, detectarlo y recuperarse. Esto acepta la pérdida ocasional de trabajo a cambio de menos restricciones sobre las solicitudes ordinarias de recursos. La detección solo es útil cuando existe una acción de recuperación viable.
Encuentra ciclos bloqueados
Para recursos de una sola instancia, mantén un grafo de espera y busca un ciclo, por ejemplo mediante una búsqueda en profundidad. Supón que las aristas son T1 → T2, T2 → T3, T3 → T1 y T4 → T2. T1, T2 y T3 forman el ciclo. T4 está bloqueado detrás de él; la propia ejecución de T4 no puede liberar un recurso que rompa ese ciclo.
Con varias instancias, un ciclo no basta. Usa Available, Allocation y la solicitud pendiente actual Request de cada hilo. Inicializa Work con Available. Busca repetidamente una solicitud que quepa, simula la finalización y añade a Work la Allocation de ese hilo. A diferencia de la evitación, esta comprobación usa lo que los hilos esperan ahora, no su posible demanda máxima futura.
El detector estándar de recursos aparta inicialmente a los hilos que no retienen recursos: no pueden aportar recursos retenidos a un ciclo de recursos. Entre los hilos que retienen recursos, los que quedan sin terminar en la comprobación están interbloqueados según el modelo. Otros hilos aún pueden ser incapaces de avanzar porque dependen de ese conjunto. Distinguir el ciclo de sus dependientes bloqueados ayuda a elegir una víctima de recuperación eficaz.
Recupérate sin corromper el estado compartido
Un sistema puede terminar uno o más procesos, recuperar recursos revirtiendo el estado a un punto de control o reiniciar. Elige las víctimas según el coste de recuperación, la prioridad, el trabajo ya realizado y los recursos retenidos. Después de eliminar una víctima, vuelve a comprobar: varios ciclos independientes pueden requerir más de una acción de recuperación.
Terminar un hilo arbitrario no basta si deja un mutex adquirido o una estructura compartida modificada a medias. La recuperación debe liberar o reconstruir los recursos y restaurar un estado válido. Los sistemas de transacciones pueden abortar una transacción interbloqueada porque la reversión forma parte de su diseño. Una aplicación ordinaria suele necesitar en su lugar un reinicio controlado del proceso.
Interpreta los tiempos de espera agotados con cuidado
pthread_mutex_timedlock() puede devolver ETIMEDOUT al alcanzar un plazo. Un temporizador de vigilancia puede reiniciar un dispositivo que deja de informar de su progreso. Ambos detectan una espera excesiva; una espera larga también puede deberse a una entrada/salida lenta o a la planificación. Un tiempo de espera agotado es una señal para investigar o recuperarse, no un diagnóstico inequívoco de un ciclo.
| Estrategia | Qué necesita | Coste principal |
|---|---|---|
| Prevención | Una regla de diseño que elimine una condición necesaria. | Menos flexibilidad o concurrencia. |
| Evitación | Demandas máximas futuras y una comprobación de seguridad. | Solicitudes retrasadas y trabajo de gestión. |
| Detección y recuperación | Un detector y una vía de recuperación válida. | Coste de comprobación y posible pérdida de trabajo. |
| Ignorar interbloqueos infrecuentes | Un fallo y un coste de reinicio tolerables. | Interrupciones ocasionales. |
Linux y Windows generalmente no encuentran ni reparan ciclos entre los propios cerrojos de una aplicación. El programa debe implementar la estrategia elegida. Si la recuperación selecciona repetidamente el mismo trabajo como víctima, ese trabajo puede sufrir inanición aunque se resuelva cada interbloqueo individual.
Puntos clave
- La detección encuentra un interbloqueo existente; la prevención impide que se forme.
- La detección con varias instancias usa las solicitudes actuales, no el Need máximo.
- Distingue un ciclo de recursos de los hilos bloqueados detrás de él.
- La recuperación debe restaurar los recursos y el estado compartido a una situación válida, no solo detener un hilo.
- Un tiempo de espera agotado también puede indicar una operación lenta.
5Mantén ágil un bucle de eventos
La concurrencia puede surgir de un hilo que atiende muchas tareas mientras se solapan sus esperas de entrada/salida. Un bucle de eventos elige una tarea disponible y ejecuta un manejador breve, y luego vuelve a esperar el siguiente evento. Esto elimina la necesidad de cerrojos entre los manejadores síncronos de ese bucle, pero hace que cada operación bloqueante retrase a todos. Las comprobaciones de disponibilidad, la entrada/salida asíncrona y las continuaciones guardadas mantienen el bucle en marcha; los múltiples núcleos y el estado compartido siguen requiriendo cuidado.
5.1 Gestiona descriptores disponibles sin bloquearte
Un bucle de eventos espera eventos y despacha manejadores breves en un solo hilo. El programa elige explícitamente qué evento disponible gestionar a continuación. Node.js y nginx usan diseños basados en eventos para atender muchas conexiones sin asignar un hilo dedicado a cada una.
while (1) {
events = get_events();
/* handle each ready event, then return to the loop */
}
Los manejadores síncronos del mismo bucle se ejecutan de uno en uno. Las instrucciones ordinarias de un manejador no son interrumpidas por otro manejador de ese bucle. El bucle sigue dependiendo de que los manejadores retornen con rapidez.
Construye y examina los conjuntos de select
select() acepta conjuntos de descriptores que se deben vigilar para lectura, escritura y condiciones excepcionales, además de un tiempo de espera. Su argumento nfds es uno más que el descriptor vigilado más alto. Por tanto, vigilar los descriptores 3, 5 y 9 necesita nfds = 10, no 3.
FD_ZERO(&readset);
FD_SET(3, &readset);
FD_SET(5, &readset);
FD_SET(9, &readset);
int n = select(10, &readset, NULL, NULL, NULL);
/* If n > 0, test each watched descriptor with FD_ISSET. */
Un tiempo de espera nulo espera hasta que algo esté disponible; un tiempo de espera cero comprueba y retorna inmediatamente. select() sobrescribe los conjuntos con los descriptores disponibles, por lo que debes reconstruirlos antes de cada llamada. Su retorno positivo cuenta los bits establecidos entre los conjuntos devueltos; un descriptor disponible en dos conjuntos cuenta dos veces. Cero significa que se agotó el tiempo de espera; −1 significa error.
La disponibilidad no es una transferencia de datos. El programa aún llama a read() o recv(). El fin de la entrada puede aparecer como disponibilidad para lectura; una lectura que devuelve 0 significa que el otro extremo ha cerrado su lado de envío. Gestiona ese resultado en vez de volver a despachar repetidamente la misma entrada terminada.
Mantén los manejadores breves y no bloqueantes
Configura los sockets con O_NONBLOCK. Cuando una operación no puede continuar, devuelve −1 con errno establecido en EAGAIN o EWOULDBLOCK, lo que permite al manejador retornar al bucle. La disponibilidad puede cambiar antes de la operación, por lo que el modo no bloqueante también protege frente a una espera inesperada después de un aviso de disponibilidad.
Una lectura bloqueante dentro de un manejador retrasa todos los eventos en cola. Con 100 solicitudes en cola y una lectura bloqueante de disco de 10 ms por manejador, la última solicitud espera unos 990 ms antes de que comience su propia lectura; el lote tarda unos 1000 ms. El hilo pasó la mayor parte de ese tiempo esperando mientras el trabajo disponible permanecía en cola.
Elige una interfaz de disponibilidad
| Interfaz | Cómo se representa el conjunto vigilado | Propiedad importante |
|---|---|---|
select() | Conjuntos de bits de descriptores reconstruidos para cada llamada. | Los valores de los descriptores deben caber en FD_SETSIZE, normalmente 1024. |
poll() | Un arreglo de descriptores y eventos solicitados y devueltos. | No existe un límite fijo equivalente para el valor de los descriptores. |
epoll / kqueue | Un conjunto de interés registrado y mantenido por el núcleo. | Entrega eficiente de eventos para conjuntos vigilados grandes. |
select() y poll() examinan el conjunto vigilado proporcionado en cada llamada, por lo que su trabajo aumenta con su tamaño. Linux ofrece epoll; FreeBSD y macOS ofrecen kqueue. Vigilar solo unos pocos descriptores no hace que un valor de descriptor alto sea válido para un fd_set de tamaño fijo.
Puntos clave
- Un bucle despacha los manejadores síncronos de uno en uno.
- Para select, nfds es el descriptor vigilado más alto más uno.
- Reconstruye los conjuntos de select después de cada llamada.
- La disponibilidad requiere una llamada posterior de entrada/salida; una lectura que devuelve 0 indica el fin de la entrada.
- Los sockets no bloqueantes permiten que los manejadores retornen cuando una operación tendría que esperar.
- FD_SETSIZE restringe los valores de los descriptores, no solo cuántos se vigilan.
5.2 Guarda el trabajo que continúa después de la entrada/salida
La disponibilidad de la red indica a un bucle cuándo puede intentar una operación de entrada/salida. Los archivos regulares de disco son diferentes: select() y poll() los indican como disponibles incluso cuando acceder a sus datos aún requiere esperar al almacenamiento. Para seguir atendiendo otros eventos, envía el trabajo de forma asíncrona y gestiona su resultado más tarde.
Envía, espera en otra parte y recoge
La entrada/salida asíncrona POSIX almacena una solicitud en struct aiocb. Los campos importantes son aio_fildes para el descriptor de archivo, aio_offset para la posición en el archivo, aio_buf para el destino y aio_nbytes para la longitud solicitada.
/* cb describes a read request */
aio_read(&cb);
/* Return to other work. Later: */
int status = aio_error(&cb);
/* EINPROGRESS: still running; 0: succeeded; otherwise an error. */
/* After completion, aio_return(&cb) collects its result. */
aio_read() pone la solicitud en cola y retorna. La finalización es un evento separado. Cuando aio_error() indica que ha terminado, aio_return() proporciona el resultado, como los bytes leídos tras una lectura satisfactoria. Una solicitud puede fallar y una lectura satisfactoria puede devolver menos bytes de los solicitados. El registro de la solicitud y su búfer deben permanecer válidos mientras la operación esté pendiente.
Examinar repetidamente cada solicitud en un bucle continuo desperdicia tiempo de CPU. aio_sigevent puede configurar una señal o una función de retorno de finalización. io_uring de Linux usa colas de envío y finalización; los puertos de finalización de entrada/salida de Windows proporcionan despacho basado en la finalización. Estas interfaces permiten al programa actuar sobre el trabajo terminado sin volver a comprobar constantemente cada solicitud pendiente.
Conserva el estado durante el intervalo
Un hilo bloqueado mantiene las variables locales de su función en su pila. Un manejador de eventos que envía una operación de entrada/salida y retorna debe guardar lo que necesita el manejador posterior. Este trabajo de gestión explícito es la gestión manual de la pila; el estado guardado es una continuación.
Supón que un servidor lee el descriptor de archivo 12 para un cliente en el socket 7. Al enviar la solicitud guarda un registro que asocia esa solicitud de lectura con el socket 7 y el estado de su respuesta. Cuando llega la finalización, el bucle encuentra el registro, comprueba el resultado de la lectura y continúa enviando la respuesta al cliente correcto. Los registros separados evitan que varias solicitudes pendientes confundan sus destinos.
La continuación debe estar ligada a la operación concreta, no solo a un identificador que podría reutilizarse después. Mantén válida la referencia al cliente hasta que se gestione la finalización y elimina el registro cuando termine la tarea. Esos detalles del tiempo de vida sustituyen las suposiciones que una función bloqueante de ejecución secuencial podría mantener en su pila.
Una sintaxis legible puede ocultar una suspensión
Las funciones de retorno, las promesas y async/await automatizan gran parte de este guardado de estado. En JavaScript, const data = await readFile(path); suspende esa función mientras su operación de entrada/salida está pendiente. Puede ejecutarse otro trabajo del bucle y la función se reanuda más tarde con su estado local restaurado.
Asíncrono significa que quien llama puede continuar antes de que termine la operación. No significa que sus manejadores se ejecuten en varios núcleos de CPU a la vez. Tampoco hace que el código que consume mucha CPU después de un await ceda automáticamente: ese código sigue ocupando el hilo que lo ejecuta hasta que retorna o vuelve a suspenderse.
Puntos clave
- La disponibilidad de un archivo regular no garantiza que una lectura de disco evite la espera.
- La entrada/salida asíncrona separa el envío de la finalización y la recogida del resultado.
- Una continuación guarda el estado necesario después de que retorne el manejador original.
- Mantén válidos los registros de solicitudes y los búferes hasta la finalización.
- Async/await guarda el estado durante la suspensión; no crea ejecución multinúcleo.
5.3 Elige eventos, hilos o un diseño híbrido
Un bucle de eventos es adecuado para muchas conexiones inactivas la mayor parte del tiempo con pequeñas ráfagas de trabajo. Un diseño basado en hilos mantiene juntos el flujo de control de una tarea y su pila. La mejor elección depende del tiempo de espera, el trabajo de CPU, la memoria y cómo evolucionará el código.
Compara el trabajo que implica cada diseño
| Aspecto | Diseño basado en eventos | Diseño basado en hilos |
|---|---|---|
| Conexiones mayormente inactivas | Pequeño registro de estado por conexión. | Un hilo tiene sobrecarga de pila y planificación. |
| Trabajo bloqueante | Derivarlo o dividirlo en torno a su finalización. | El hilo que llama puede esperar mientras otros se ejecutan. |
| Varios núcleos de CPU | Ejecutar varios bucles o usar trabajadores. | Planificar los hilos listos entre los núcleos. |
| Datos compartidos modificables | Un bucle síncrono evita el solapamiento entre manejadores. | Los accesos compartidos necesitan sincronización. |
| Una función pasa a ser bloqueante | Quienes la llaman pueden necesitar una reestructuración con continuaciones. | Puede mantenerse el flujo de control secuencial. |
Un servidor de chat con 50 000 conexiones mayormente inactivas puede beneficiarse de usar registros de eventos en vez de una pila de hilo dedicada por conexión. Un procesador de imágenes que realiza cálculos intensos por solicitud necesita suficientes trabajadores para usar los núcleos disponibles; un solo bucle ocupado no puede ejecutar esos cálculos en paralelo.
Las esperas ocultas siguen importando
Un manejador puede detenerse sin una llamada evidente a una API bloqueante. Acceder a una página que se ha paginado al disco puede provocar una lectura de disco. Durante ese fallo de página, el hilo del bucle espera. mlock() puede mantener residentes las páginas necesarias cuando el sistema permite la cantidad solicitada de memoria fijada. Eliminar las llamadas bloqueantes explícitas no elimina todas las causas de retraso.
Los cambios de código pueden introducir otra espera oculta. Si una función auxiliar que antes respondía de inmediato empieza a obtener datos, quien la llama desde código basado en eventos debe dividir su trabajo en torno a una continuación o derivar la llamada. La pila de un hilo bloqueante ya almacena dónde reanudar y el estado local que necesita.
Combina las ventajas
Un diseño híbrido mantiene la gestión de la disponibilidad de red en el bucle de eventos y envía las tareas bloqueantes o que consumen mucha CPU a un grupo de trabajadores. Las finalizaciones vuelven al bucle para gestionar la respuesta. Node.js usa el grupo de hilos de libuv para las operaciones del sistema de archivos, por lo que una interfaz asíncrona puede implementarse mediante hilos en segundo plano.
Para usar varios núcleos, un servidor puede ejecutar un bucle de eventos por núcleo en hilos o procesos separados. Si esos bucles comparten datos modificables, necesitan los cerrojos habituales u otro protocolo de sincronización. La ventaja de no necesitar cerrojos se aplica a los manejadores síncronos de un solo bucle, no a todas las partes de un servidor basado en eventos.
Una tarea puede intercalarse a través de await
Supón que queda una unidad en existencias. Un manejador comprueba la disponibilidad, espera el pago y luego descuenta la unidad. Mientras está suspendido, otro manejador puede comprobar las mismas existencias y también iniciar el pago. Ambas comprobaciones pueden tener éxito antes de cualquiera de los descuentos. No hay dos manejadores síncronos ejecutándose simultáneamente, pero la operación completa de comprobación y actualización no es atómica.
Reserva la unidad mediante un cambio de estado sincronizado antes de ceder la ejecución, con una vía para liberar la reserva si falla el pago, o vuelve a comprobar bajo un protocolo que impida una segunda venta. Mantener un mutex adquirido durante una espera larga puede crear otros problemas de progreso, por lo que debes diseñar explícitamente la transición de estado y su recuperación. El código basado en eventos sigue necesitando razonar sobre qué hechos siguen siendo verdaderos tras cada suspensión.
Puntos clave
- Los eventos son adecuados para muchas conexiones inactivas; el trabajo que consume mucha CPU necesita trabajadores en paralelo.
- Un bucle de eventos usa un núcleo; los datos compartidos entre bucles necesitan sincronización.
- Los fallos de página y las funciones auxiliares que pasan a ser bloqueantes pueden detener un bucle.
- Los servidores híbridos derivan el trabajo bloqueante y devuelven las finalizaciones al bucle.
- Una tarea que abarca un await puede sufrir una violación de atomicidad entre la comprobación y la acción.
Ejemplos de Semáforos e interbloqueo
1De un indicador de finalización a una cola funcional
12 pasos
- 1IntroducciónEspera sin desperdiciar una CPU
Un padre necesita el resultado de su hijo, pero el hijo puede terminar antes o después de que el padre empiece a esperar. Un protocolo funcional debe gestionar ambos órdenes.
Usa un indicador compartido
donepara recordar la finalización, un mutex para protegerlo y una variable de condición para permitir que el padre se suspenda. Sigue cómo pasa el mutex de un hilo a otro a medida que se ejecutan. - 2EjemploEl padre llega primero
Inicialmente
done = 0. El padre adquierem, comprueba el indicador y llama await(&c, &m)dentro de un buclewhile.La espera libera el mutex e inicia la espera de forma atómica. El hijo puede ahora adquirir
m. No hay ningún intervalo sin el mutex adquirido entre la comprobación de la condición y el inicio de la espera en el que el hijo pueda enviar una notificación que el padre pierda. - 3EjemploEl hijo registra la finalización
El hijo adquiere
m, almacenadone = 1, notifica ency libera el mutex. La notificación deja listo al padre; no hace que se ejecute de inmediato.El padre vuelve a adquirir
mantes de que retorne la espera, comprueba de nuevodoney continúa porque ahora está registrado que el hijo terminó. Ambos hilos usan el mismo mutex para el indicador compartido. - 4DesafíoInvierte su orden de llegada
El hijo termina y notifica antes de que el padre haya llegado a su espera. ¿Qué impide que el padre se suspenda para siempre?
---reveal---
El hijo ya ha establecido
done = 1con el mutex adquirido. El padre comprueba ese indicador mientras tiene adquirido el mutex y omite la espera. La notificación temprana no se guarda; el indicador de estado conserva la información pese al distinto orden de llegada. - 5EjemploUn despertar no reserva un elemento
C1 está esperando un búfer de una posición. Un productor inserta un elemento y notifica a C1. Antes de que C1 vuelva a adquirir el mutex, C2 lo adquiere y extrae ese elemento.
Según la semántica de Mesa, C1 solo está listo para competir. Debe volver a comprobar el búfer después de adquirir el mutex. El retorno de la espera no puede sustituir la comprobación de si el elemento aún existe.
- 6DesafíoElige la nueva comprobación correcta
En ese seguimiento, C1 usó
if (count == 0) wait(...). Ahora retorna después de que C2 haya vaciado la posición. ¿Qué cambio en el código impide una extracción no válida?---reveal---
Usa
while (count == 0) wait(...). Después de volver a adquirir el mutex, C1 ve el búfer vacío y espera de nuevo. El mismo bucle también gestiona los despertares espurios, por lo que es necesario incluso sin un segundo consumidor. - 7EjemploDespierta a todos los hilos que pueden continuar
Un asignador tiene 0 bytes libres. Ta espera 100 bytes y Tb espera 10. Liberar 50 permite continuar solo a Tb, pero una notificación podría despertar a Ta.
Notificar a todos llega a ambos. Cada uno vuelve a adquirir el mutex y comprueba la cantidad que solicitó. Tb puede tomar 10; Ta vuelve a esperar. El bucle del predicado garantiza la corrección mientras que la notificación amplia evita dejar suspendido a un hilo que puede continuar.
- 8EjemploMantén el orden de la cola al volver al inicio
La capacidad es 3. Inserta 7, 8, 9; extrae 7; inserta 10. El arreglo físico es ahora
[10, 8, 9].El índice de lectura apunta a la posición 1, por lo que el orden de extracción es 8, 9, 10. Avanza ambos índices módulo la capacidad y cambia
countcon el mutex adquirido. La visualización del arreglo de izquierda a derecha no es necesariamente el orden lógico de la cola. - 9Idea claveDa a cada papel su propia cola
Un productor espera en
emptymientras el búfer está lleno, inserta y luego notifica enfill. Un consumidor espera enfillmientras está vacío, extrae y luego notifica enempty. Ambas variables de condición usan el mutex del búfer.Las colas separadas son importantes: una notificación de elemento llega a un consumidor y una notificación de espacio llega a un productor. Una sola cola de notificación compartida podría elegir un hilo cuya condición sigue siendo falsa.
- 10DesafíoEncuentra al productor suspendido
Un búfer de una posición usa una cola de condición compartida y bucles
while. P se suspende porque el búfer está lleno. C1 extrae el elemento, notifica a C2 y luego espera porque está vacío. C2 también lo encuentra vacío y espera. ¿Por qué sigue sin usarse el espacio?---reveal---
P nunca recibió la notificación de espacio. Todo el trabajo que podía ejecutarse se ha detenido aunque un productor podría continuar. Las colas separadas de productores y consumidores dirigen la notificación de C1 a P.
- 11DesafíoTermina el trabajo sin perder elementos
Dos productores siguen generando solicitudes. Un coordinador planea añadir ahora un marcador de parada por cada consumidor. ¿Qué debe cambiar y por qué?
---reveal---
Primero espera a que todos los productores terminen. Solo entonces pon en cola los marcadores de parada. De lo contrario, los consumidores pueden tomar los marcadores y salir mientras los productores aún tienen solicitudes reales que insertar. Se sigue usando la sincronización normal de la cola para insertar y extraer los marcadores.
- 12RepasoRevisa una espera en una condición
Para cada espera, identifica el estado, el mutex y la cola. Mantén la comprobación del predicado bajo el mutex, inicia la espera sin un intervalo con el mutex liberado y vuelve a comprobar después de cada retorno.
Luego examina la ruta de notificación: ¿cambió el estado y puede llegar la notificación a un hilo que ahora puede continuar? Estas preguntas revelan tanto los despertares perdidos como las detenciones del búfer por despertar al papel equivocado.
2Sigue los permisos en protocolos reales
15 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónDefine qué significa un permiso
- 2EjemploSigue un semáforo binario
- 3DesafíoCorrige un recuento inicial incorrecto
- 4EjemploRecuerda una finalización temprana
- 5EjemploLimita una fase que consume mucha memoria
- 6DesafíoSepara la admisión de la gestión de la lista
- 7ConexiónEncuéntrense antes de continuar
- 8ExplicaciónReserva, actualiza y publica
- 9EjemploSigue a un consumidor bloqueado
- 10DesafíoDetecta la dependencia del cerrojo
- 11ExplicaciónPermite que un grupo de lectores tenga un permiso
- 12EjemploObserva al escritor en espera
- 13DesafíoGestiona una actualización necesaria
- 14DesafíoMantén ambos tipos de protección
- 15RepasoRevisa cada permiso adquirido
3Diagnostica el intercalado oculto
9 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónPregunta qué supone el código
- 2EjemploSitúa un cambio dentro del intervalo
- 3EjemploHaz indivisible la decisión numérica
- 4DesafíoEncuentra el límite que falta de la sección crítica
- 5EjemploUn nuevo trabajador puede ejecutarse primero
- 6ExplicaciónPublica un estado de finalización
- 7DesafíoSustituye una estimación del tiempo
- 8DesafíoElige entre las dos correcciones
- 9RepasoComprueba la garantía, no el retraso
4De un ciclo de cerrojos a una solicitud segura
17 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónDistingue tres estrategias
- 2EjemploCierra un ciclo de dos hilos
- 3ExplicaciónNombra las condiciones necesarias
- 4DesafíoDistingue una cadena de un ciclo
- 5EjemploOrdena ambas direcciones de la misma forma
- 6ConsejoLibera lo adquirido parcialmente antes de reintentar
- 7DesafíoRevisa la función auxiliar oculta
- 8EjemploCalcula las demandas restantes
- 9EjemploEncuentra un hilo que pueda terminar primero
- 10EjemploCompleta la secuencia de seguridad
- 11Idea claveUna concesión de recursos libres aún puede ser insegura
- 12DesafíoCompara los recursos por tipo
- 13ConexiónDetecta la espera que existe ahora
- 14ExplicaciónRecupérate hasta alcanzar un estado válido
- 15DesafíoSelecciona una víctima de recuperación útil
- 16DesafíoInterpreta un resultado inseguro
- 17RepasoAjusta la comprobación a la pregunta
5Atiende eventos mientras la entrada/salida está pendiente
13 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónLleva la espera al límite del bucle
- 2EjemploConstruye el conjunto vigilado de select
- 3ExplicaciónRealiza la lectura efectiva
- 4DesafíoComprueba un descriptor de valor alto
- 5ExplicaciónNo confundas la disponibilidad de un archivo con la finalización
- 6EjemploGuarda el destino de la respuesta
- 7DesafíoInterpreta una operación pendiente
- 8ConexiónDeja que await conserve el estado local
- 9EjemploUsa trabajadores para el trabajo costoso
- 10ConsejoPresta atención a una espera implícita
- 11DesafíoEncuentra el intervalo a través de await
- 12DesafíoMantén visible el límite entre núcleos
- 13RepasoRevisa cada punto de suspensión
Ejercicios de Semáforos e interbloqueo
Set 1: Sigue el protocolo a través de una planificación10 Preguntas de Práctica
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
writelock. El grupo tiene un permiso de recurso, no un permiso separado que el escritor deba recoger de cada lector.Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Set 2: Esperas, permisos y dependencias10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 3: Aplica las reglas cuando cambian las condiciones10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 4: Analiza correcciones incompletas10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 5: Aplica el razonamiento a nuevos diseños10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
¿Te atascas con una pregunta tuya? Ayuda con Tareas
Términos Clave
A
Actualización perdida
Admisión equitativa
Algoritmo del banquero
Ausencia de expropiación
B
Bloque de control de entrada/salida asíncrona
Bloqueo activo
Bloqueo implícito
Bucle de eventos
Búfer acotado
Búfer circular
C
Cerrojo de lectores-escritores
Cola de finalización
Comparar e intercambiar
Comprobar y actuar
Concurrencia híbrida
Condición de cobertura
Conjunto de descriptores
select() lo sustituye por el subconjunto disponible.Continuación
Control de admisión
Conversión de cerrojo de lectura a escritura
D
Despertar espurio
Despertar perdido
Detección de interbloqueos
Disponibilidad para entrada/salida
E
Entrada/salida asíncrona
Entrada/salida no bloqueante
EAGAIN y el manejador retorna al bucle de eventos.epoll
Espera a la terminación de un hilo
Espera aleatoria antes del reintento
Espera circular
Estado inseguro
Estado seguro
Evitación de interbloqueos
Exclusión mutua
G
Gestión manual de la pila
Grafo de espera
Grupo de trabajadores
I
Inanición
Inanición de los escritores
Indicador de disponibilidad
ready solo después de asignar el registro que usará el trabajador.Intento de adquisición sin espera
Interbloqueo
Intercalado
io_uring
K
kqueue
L
Liberación y espera atómicas
Limitación de la concurrencia
M
Marcador de fin de datos
Modelo de contador con signo
Monitor
wait().Mutex del búfer
fill_ptr, bajo el mismo mutex.N
Notificación a todos los hilos en espera
Notificación a un hilo en espera
O
Obtener y sumar
Operación de espera del semáforo
Operación de señalización del semáforo
Orden de adquisición de cerrojos
P
Paralelismo multinúcleo
Permiso
poll
Predicado
done != 0 permite que un padre compruebe si su hijo ha terminado.Preferencia a los escritores
Preferencia a los lectores
Prevención de interbloqueos
Problema del productor-consumidor
Propiedad del semáforo
Punto de encuentro
R
Reserva de recursos
Retención y espera
Reversión
S
Secuencia segura
select
Semáforo
Semáforo binario
Semáforo contador
Semáforo de elementos disponibles
Semáforo de espacios libres
Semántica de Hoare
Semántica de Mesa
Suspensión
T
Temporizador de vigilancia
Tiempo de espera agotado
V
Variable de condición
Variable de estado
Vector de necesidades
Vector de recursos disponibles
Vector de trabajo
Víctima seleccionada para la recuperación
Violación de atomicidad
Violación del orden de ejecución
Sobre esta unidad
Crea tus propios Materiales de Estudio
Convierte tus notas, diapositivas y documentos en Guías de Estudio, Preguntas de Práctica, Flashcards y 6 herramientas más.