Variables de condición, semáforos e interbloqueo

Los cerrojos evitan que los hilos interfieran entre sí, pero los hilos también necesitan esperarse unos a otros. Este material muestra cómo las variables de condición y los semáforos permiten que un hilo se suspenda hasta que haya datos disponibles, se libere una posición o termine otro hilo, y los utiliza para resolver los problemas del productor-consumidor y de los lectores-escritores. Después aborda los errores que aparecen en código concurrente real: violaciones de atomicidad y del orden de ejecución, e interbloqueos, con métodos para prevenirlos, evitarlos y detectarlos. Termina con la concurrencia basada en eventos, en la que un solo hilo ejecuta un bucle de eventos en vez de usar muchos hilos. Los ejemplos usan C con hilos POSIX y se siguen paso a paso.

IntermedioUnidad 6 de 9 del curso Curso de Sistemas operativos

Lo que aprenderás

  1. 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 activa
      Explica 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 todos
      Aplica 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ón
      Construye 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.
  2. 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 cerrojo
      Describe 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áforos
      Usa 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áforos
      Resuelve 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-escritores
      Permite 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.
  3. 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 atomicidad
      Reconoce 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ón
      Reconoce 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.
  4. 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 interbloqueo
      Define 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 necesaria
      Diseñ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 banquero
      Usa 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 sistema
      Permite 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.
  5. 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 poll
      Escribe 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 estado
      Inicia 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 hilos
      Compara 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.

Guía de estudio de Semáforos e interbloqueo

5 capítulos

1

Espera 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ánticaQué proporciona una notificación al hilo en espera
MesaDisponibilidad para ejecutarse; el hilo en espera debe volver a adquirir el mutex y comprobar el estado.
HoareEntrega 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.
2

Cuenta 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ósitoValor inicialSignificado
Cerrojo1Un hilo puede entrar inmediatamente.
Ordenación de eventos0El evento no ha ocurrido.
Conjunto de recursosNSe 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:

  1. T0 espera: 1 pasa a 0, por lo que T0 entra.
  2. T1 espera: 0 pasa a −1, por lo que T1 se bloquea.
  3. T0 señaliza: −1 pasa a 0, lo que permite a T1 continuar.
  4. 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íticaElección de admisiónRiesgo
Preferencia a los lectoresLos nuevos lectores pueden unirse a los lectores activos.Los escritores pueden sufrir inanición.
Preferencia a los escritoresLos escritores en espera impiden la entrada de nuevos lectores.Los lectores pueden sufrir inanición.
Admisión equitativaLos 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.
3

Encuentra 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 ocultaTipo de errorGarantía necesaria
«Nada cambia entre esta comprobación y la acción».Violación de atomicidadUna operación protegida.
«La inicialización termina antes de este uso».Violación del orden de ejecuciónUna 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.
4

Razona 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 necesariaSignificado en el ejemplo de cerrojos
Exclusión mutuaSolo un hilo tiene adquirido un cerrojo determinado.
Retención y esperaCada uno tiene adquirido un cerrojo mientras espera otro.
Ausencia de expropiaciónUn cerrojo adquirido no se retira por la fuerza.
Espera circularCada 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écnicaPrincipal contrapartida
Adquisición ordenadaTodas las rutas del código deben respetar el orden.
Adquisición simultáneaLos recursos deben conocerse y pueden quedar sin usar.
Trylock y reintentoHay que atender al trabajo de reintento y al bloqueo activo.
Actualización sin cerrojosEl 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):

HiloAllocationMaxNeed
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.

  1. T1 cabe en (3, 2, 2). Devolver (2, 0, 1) da Work (5, 2, 3).
  2. T0 cabe. Devolver (1, 1, 0) da (6, 3, 3).
  3. T2 cabe. Devolver (1, 1, 2) da (7, 4, 5).
  4. 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.

EstrategiaQué necesitaCoste principal
PrevenciónUna regla de diseño que elimine una condición necesaria.Menos flexibilidad o concurrencia.
EvitaciónDemandas máximas futuras y una comprobación de seguridad.Solicitudes retrasadas y trabajo de gestión.
Detección y recuperaciónUn detector y una vía de recuperación válida.Coste de comprobación y posible pérdida de trabajo.
Ignorar interbloqueos infrecuentesUn 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.
5

Manté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
InterfazCómo se representa el conjunto vigiladoPropiedad 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 / kqueueUn 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
AspectoDiseño basado en eventosDiseño basado en hilos
Conexiones mayormente inactivasPequeño registro de estado por conexión.Un hilo tiene sobrecarga de pila y planificación.
Trabajo bloqueanteDerivarlo o dividirlo en torno a su finalización.El hilo que llama puede esperar mientras otros se ejecutan.
Varios núcleos de CPUEjecutar varios bucles o usar trabajadores.Planificar los hilos listos entre los núcleos.
Datos compartidos modificablesUn bucle síncrono evita el solapamiento entre manejadores.Los accesos compartidos necesitan sincronización.
Una función pasa a ser bloqueanteQuienes 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

5 Lecciones Interactivas

1

De un indicador de finalización a una cola funcional

12 pasos
Sigue la suspensión, el despertar, la nueva comprobación y la dirección de las notificaciones entre productores y consumidores.
  1. 1
    Introducció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 done para 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.

  2. 2
    EjemploEl padre llega primero

    Inicialmente done = 0. El padre adquiere m, comprueba el indicador y llama a wait(&c, &m) dentro de un bucle while.

    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.

  3. 3
    EjemploEl hijo registra la finalización

    El hijo adquiere m, almacena done = 1, notifica en c y libera el mutex. La notificación deja listo al padre; no hace que se ejecute de inmediato.

    El padre vuelve a adquirir m antes de que retorne la espera, comprueba de nuevo done y continúa porque ahora está registrado que el hijo terminó. Ambos hilos usan el mismo mutex para el indicador compartido.

  4. 4
    Desafí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 = 1 con 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.

  5. 5
    EjemploUn 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.

  6. 6
    Desafí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.

  7. 7
    EjemploDespierta 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.

  8. 8
    EjemploManté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 count con el mutex adquirido. La visualización del arreglo de izquierda a derecha no es necesariamente el orden lógico de la cola.

  9. 9
    Idea claveDa a cada papel su propia cola

    Un productor espera en empty mientras el búfer está lleno, inserta y luego notifica en fill. Un consumidor espera en fill mientras está vacío, extrae y luego notifica en empty. 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.

  10. 10
    Desafí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.

  11. 11
    Desafí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.

  12. 12
    RepasoRevisa 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.

2

Sigue los permisos en protocolos reales

15 pasos
Elige recuentos, sigue las transferencias y mantén separadas las reservas de recursos y la exclusión mutua.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónDefine qué significa un permiso
  2. 2
    EjemploSigue un semáforo binario
  3. 3
    DesafíoCorrige un recuento inicial incorrecto
  4. 4
    EjemploRecuerda una finalización temprana
  5. 5
    EjemploLimita una fase que consume mucha memoria
  6. 6
    DesafíoSepara la admisión de la gestión de la lista
  7. 7
    ConexiónEncuéntrense antes de continuar
  8. 8
    ExplicaciónReserva, actualiza y publica
  9. 9
    EjemploSigue a un consumidor bloqueado
  10. 10
    DesafíoDetecta la dependencia del cerrojo
  11. 11
    ExplicaciónPermite que un grupo de lectores tenga un permiso
  12. 12
    EjemploObserva al escritor en espera
  13. 13
    DesafíoGestiona una actualización necesaria
  14. 14
    DesafíoMantén ambos tipos de protección
  15. 15
    RepasoRevisa cada permiso adquirido
3

Diagnostica el intercalado oculto

9 pasos
Distingue una operación que debe permanecer unida de una operación que debe ocurrir primero.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónPregunta qué supone el código
  2. 2
    EjemploSitúa un cambio dentro del intervalo
  3. 3
    EjemploHaz indivisible la decisión numérica
  4. 4
    DesafíoEncuentra el límite que falta de la sección crítica
  5. 5
    EjemploUn nuevo trabajador puede ejecutarse primero
  6. 6
    ExplicaciónPublica un estado de finalización
  7. 7
    DesafíoSustituye una estimación del tiempo
  8. 8
    DesafíoElige entre las dos correcciones
  9. 9
    RepasoComprueba la garantía, no el retraso
4

De un ciclo de cerrojos a una solicitud segura

17 pasos
Sigue un interbloqueo, elimina una condición, comprueba la seguridad de los recursos y planifica la recuperación.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónDistingue tres estrategias
  2. 2
    EjemploCierra un ciclo de dos hilos
  3. 3
    ExplicaciónNombra las condiciones necesarias
  4. 4
    DesafíoDistingue una cadena de un ciclo
  5. 5
    EjemploOrdena ambas direcciones de la misma forma
  6. 6
    ConsejoLibera lo adquirido parcialmente antes de reintentar
  7. 7
    DesafíoRevisa la función auxiliar oculta
  8. 8
    EjemploCalcula las demandas restantes
  9. 9
    EjemploEncuentra un hilo que pueda terminar primero
  10. 10
    EjemploCompleta la secuencia de seguridad
  11. 11
    Idea claveUna concesión de recursos libres aún puede ser insegura
  12. 12
    DesafíoCompara los recursos por tipo
  13. 13
    ConexiónDetecta la espera que existe ahora
  14. 14
    ExplicaciónRecupérate hasta alcanzar un estado válido
  15. 15
    DesafíoSelecciona una víctima de recuperación útil
  16. 16
    DesafíoInterpreta un resultado inseguro
  17. 17
    RepasoAjusta la comprobación a la pregunta
5

Atiende eventos mientras la entrada/salida está pendiente

13 pasos
Construye un bucle de disponibilidad, guarda continuaciones y localiza los riesgos de concurrencia restantes.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónLleva la espera al límite del bucle
  2. 2
    EjemploConstruye el conjunto vigilado de select
  3. 3
    ExplicaciónRealiza la lectura efectiva
  4. 4
    DesafíoComprueba un descriptor de valor alto
  5. 5
    ExplicaciónNo confundas la disponibilidad de un archivo con la finalización
  6. 6
    EjemploGuarda el destino de la respuesta
  7. 7
    DesafíoInterpreta una operación pendiente
  8. 8
    ConexiónDeja que await conserve el estado local
  9. 9
    EjemploUsa trabajadores para el trabajo costoso
  10. 10
    ConsejoPresta atención a una espera implícita
  11. 11
    DesafíoEncuentra el intervalo a través de await
  12. 12
    DesafíoMantén visible el límite entre núcleos
  13. 13
    RepasoRevisa cada punto de suspensión

Ejercicios de Semáforos e interbloqueo

50 Preguntas de Práctica en 5 sets

Set 1: Sigue el protocolo a través de una planificación10 Preguntas de Práctica

Pregunta 1 de 10

Un búfer con semáforos tiene capacidad 6 y contiene inicialmente dos elementos. No hay operaciones en curso. ¿Cuáles son sus recuentos de permisos empty y full?
  1. Empty 4; full 2.
  2. Empty 6; full 2.
  3. Empty 2; full 4.
  4. Empty 4; full 0.
Mostrar respuestaOcultar respuesta
Respuesta: A
Dos elementos publicados dan dos permisos full, y las cuatro posiciones libres dan cuatro permisos empty. Estos recuentos describen el búfer estable porque ninguna operación ha reservado un permiso sin completar su actualización. Empty no siempre es igual a la capacidad; cada posición ocupada elimina un permiso de espacio disponible.

Pregunta 2 de 10

Un búfer circular tiene capacidad 4 y comienza vacío con ambos índices en 0. Recibe 21, 22, 23, extrae dos elementos y luego recibe 24 y 25. ¿Cuál es ahora su orden lógico de extracción?
  1. 25, 23, 24
  2. 23, 24, 25
  3. 24, 25, 23
  4. 21, 24, 25
Mostrar respuestaOcultar respuesta
Respuesta: B
Las dos primeras extracciones consumen 21 y 22, dejando 23 como el elemento más antiguo. Insertar 24 y luego 25 hace que el índice de escritura vuelva al inicio, pero no mueve el índice de lectura más allá de 23. Leer el arreglo físico desde la posición 0 colocaría incorrectamente el valor 25, escrito tras volver al inicio, por delante de elementos más antiguos.

Pregunta 3 de 10

Un hilo de carga establece ready con un mutex adquirido y notifica antes de que otro hilo empiece a esperar. Más tarde, ese hilo adquiere el mismo mutex y comprueba ready antes de decidir si espera. ¿Por qué puede continuar?
  1. La variable de condición almacenó la notificación.
  2. Adquirir el mutex repite la notificación.
  3. La notificación pasó el mutex al usuario posterior.
  4. El estado ready persiste después de la notificación.
Mostrar respuestaOcultar respuesta
Respuesta: D
El indicador ready registra que la carga terminó con independencia de si alguien esperaba en el momento de la notificación. El hilo ve ese estado con el mutex adquirido y omite la espera. La variable de condición no guardó ninguna notificación y adquirir el mutex no la repite.

Pregunta 4 de 10

Tres lectores tienen adquirido el cerrojo básico con preferencia a los lectores y un escritor está esperando. Dos lectores salen. ¿Qué impide que el escritor entre todavía?
  1. El escritor debe reunir tres permisos de escritura separados.
  2. El último lector que salió ya liberó writelock.
  3. El lector restante aún mantiene el acceso del grupo.
  4. El cerrojo del contador sigue en manos del escritor en espera.
Mostrar respuestaOcultar respuesta
Respuesta: C
El contador de lectores ha bajado de tres a uno, por lo que aún no se ha producido la salida del último lector. Solo el lector que reduce el contador a cero señaliza writelock. El grupo tiene un permiso de recurso, no un permiso separado que el escritor deba recoger de cada lector.

Pregunta 5 de 10

Un saldo compartido usa cargas y restas atómicas. Dos hilos comprueban cada uno que haya al menos 45 unidades y luego restan 45; el saldo comienza en 70. ¿Qué problema persiste si ambas comprobaciones terminan primero?
  1. La segunda resta rechaza el débito.
  2. Las cargas atómicas reservan 45 unidades cada una.
  3. Ambos débitos pueden realizarse y dejar −20.
  4. Solo el primer hilo puede leer 70.
Mostrar respuestaOcultar respuesta
Respuesta: C
Ambas cargas atómicas pueden leer 70 antes de cualquiera de los débitos. Las dos restas atómicas producen entonces 25 y −20 porque la resta por sí sola no repite la decisión anterior sobre la disponibilidad. Los accesos individuales atómicos evitan actualizaciones incompletas, pero no combinan la comprobación y el débito en una sola operación condicional.

Pregunta 6 de 10

Available es (1, 1). A tiene Allocation (1, 0) y Max (2, 2); B tiene Allocation (0, 1) y Max (1, 2). ¿Qué concluye la comprobación de seguridad del banquero?
  1. Seguro; B puede terminar antes que A.
  2. Seguro; A puede terminar antes que B.
  3. Inseguro; ninguna demanda restante cabe inicialmente.
  4. Inseguro; el máximo completo de A supera Available.
Mostrar respuestaOcultar respuesta
Respuesta: A
Need de A es (1, 2), por lo que no puede comenzar con Work (1, 1). Need de B, (1, 1), sí cabe, y B devuelve su Allocation (0, 1), lo que da Work (1, 2); entonces A puede terminar. Comparar el máximo completo de A con la disponibilidad inicial cuenta incorrectamente los recursos que ya tiene e ignora la liberación posterior de B.

Pregunta 7 de 10

Una espera con plazo para adquirir un mutex devuelve ETIMEDOUT. ¿Qué establece eso?
  1. El propietario está en un ciclo de cerrojos.
  2. El mutex ha sido liberado.
  3. El estado protegido está corrupto.
  4. La espera superó su plazo.
Mostrar respuestaOcultar respuesta
Respuesta: D
El retorno establece que la adquisición solicitada no terminó dentro del plazo. Un propietario que tarda de forma normal o un retraso de planificación pueden causar el mismo resultado. Por tanto, un tiempo de espera agotado por sí solo no identifica un ciclo de interbloqueo ni indica al programa que sus datos protegidos se hayan corrompido.

Pregunta 8 de 10

Un servidor mantiene 30 000 sockets mayormente inactivos y realiza poco cálculo por mensaje. ¿Por qué puede un bucle de eventos usar menos memoria por conexión que un hilo dedicado por socket?
  1. Las conexiones no necesitan estado mientras están inactivas.
  2. Las conexiones pueden usar pequeños registros de estado guardado.
  3. Cada manejador de eventos recibe un núcleo de CPU separado.
  4. Todos los sockets comparten un descriptor de red reutilizable.
Mostrar respuestaOcultar respuesta
Respuesta: B
Un diseño basado en eventos puede almacenar un registro compacto de conexión en vez de una pila de hilo dedicada para cada socket mayormente inactivo. Las conexiones inactivas siguen necesitando estado, como sus descriptores y la posición en el protocolo. El ahorro de memoria procede de cómo se representa ese estado, no de eliminarlo ni de obtener ejecución multinúcleo automática.

Pregunta 9 de 10

Un hilo en espera lee done como falso sin el mutex. Después, el trabajador adquiere el mutex, establece done como verdadero, notifica cuando no hay nadie esperando y libera el mutex. A continuación, el hilo en espera adquiere el mutex y llama a wait sin volver a comprobar. ¿Qué fallo de dependencia ha ocurrido?
  1. Un despertar espurio antes de la notificación del trabajador.
  2. Una nueva comprobación segura de Mesa tras adquirir el mutex.
  3. Una notificación recordada que espera al siguiente hilo.
  4. Un despertar perdido tras una comprobación sin protección.
Mostrar respuestaOcultar respuesta
Respuesta: D
La notificación del trabajador no encontró a nadie esperando, y la espera posterior ignoró que el estado ya era verdadero. El hilo puede permanecer suspendido porque no se garantiza ninguna otra notificación. Adquirir el mutex solo después de la comprobación no corrige ese intervalo; comprueba el estado con el mutex adquirido y mantenlo hasta iniciar la espera.

Pregunta 10 de 10

En un grafo de espera con recursos de una sola instancia, U → V, V → W, W → V y X no tiene ninguna arista saliente. ¿Qué hilos forman el ciclo de dependencias?
  1. V y W.
  2. U, V y W.
  3. U y X.
  4. V, W y X.
Mostrar respuestaOcultar respuesta
Respuesta: A
V espera a W y W espera a V, por lo que esos dos forman el ciclo. U no puede continuar mientras su dependencia siga atrapada, pero no hay una ruta desde el ciclo de vuelta a U. X no tiene ninguna espera saliente, por lo que incluirlo confundiría a los hilos no relacionados o dependientes con los miembros del ciclo.
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

¿Qué recuento inicial del semáforo permite que exactamente un hilo entre en una sección crítica antes de una señalización?
  1. 0
  2. 2
  3. −1
  4. 1
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Una máquina tiene 12 GB de RAM. Otro trabajo necesita 3 GB y cada trabajador admitido necesita 750 MB. Usando unidades decimales del SI, ¿cuántos trabajadores caben en la memoria restante?
  1. 16
  2. 12
  3. 9
  4. 15
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un servicio tiene siete identificadores de dispositivo idénticos y disponibles. ¿Qué valor inicial del semáforo permite tomar prestados todos los identificadores disponibles?
  1. 0
  2. 1
  3. 7
  4. 8
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

¿Qué bucle debe rodear una espera en una variable de condición POSIX cuando un consumidor necesita una cola no vacía?
  1. while (count == 0)
  2. if (count == 0)
  3. while (count > 0)
  4. if (count > 0)
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Dos hilos reservan cuatro asientos cada uno. Quedan seis asientos y ambas comprobaciones de disponibilidad terminan antes de cualquiera de las reservas. ¿Qué delimitación de la sección crítica impide la sobrereserva?
  1. Una región con cerrojo para comprobar y otra para reservar.
  2. Una región con cerrojo para comprobar y reservar.
  3. Un cerrojo de lectura para toda la reserva de cada hilo.
  4. Un mutex privado separado para cada reserva.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Un hilo que genera informes necesita un total final escrito por un trabajador justo antes de que este termine. ¿Qué operación garantiza la finalización antes de que el informe lea el total?
  1. Suspenderse brevemente antes de leer.
  2. Ceder la CPU antes de leer.
  3. Adquirir un cerrojo solo para leer.
  4. Esperar a que termine el trabajador antes de leer.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

El hilo H tiene adquirido M y espera N. El hilo J tiene adquirido N y espera M. Ambos cerrojos son exclusivos y no pueden retirarse por la fuerza. ¿Qué describe a estos dos hilos?
  1. Un conjunto interbloqueado.
  2. Un grupo en inanición.
  3. Un par en bloqueo activo.
  4. Un par listo para ejecutarse.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Una transferencia adquiere los cerrojos de objetos de cuenta con claves únicas. ¿Qué regla de adquisición impide que las transferencias en direcciones opuestas creen una espera circular?
  1. Adquirir la cuenta de origen antes que la de destino.
  2. Adquirir la cuenta de destino antes que la de origen.
  3. Adquirir ambas cuentas en orden creciente de clave.
  4. Adquirir primero la cuenta con el saldo menor.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un bucle vigila los descriptores 2, 8 y 14 con select. ¿Qué valor debe pasar para nfds?
  1. 3
  2. 14
  3. 15
  4. 16
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

El manejador A envía una lectura para el socket de cliente 40. Después, el manejador B envía otra lectura para el socket 41. Ambos manejadores de finalización usan un destino global, que ahora es 41. La lectura de A termina primero. ¿A dónde se dirige la respuesta de A y qué corrección preserva a ambos clientes?
  1. A 40; seguir usando el destino global.
  2. A 41; guardar el destino por solicitud.
  3. A 40; reconstruir el conjunto de select antes de responder.
  4. A 41; mantener solo un recuento de solicitudes pendientes.
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

Un asignador compartido no tiene memoria libre. Un hilo en espera necesita 180 bytes y otro necesita 40. Una liberación añade 90 bytes y notifica solo al hilo que espera 180 bytes; no se producen más cambios. ¿Cómo debe cambiar la política de notificación para permitir que continúe un hilo que puede hacerlo?
  1. Notificar a todos después de la liberación.
  2. Notificar una vez que se acumulen 180 bytes.
  3. Omitir el mutex en la actualización.
  4. Cambiar ambos bucles while por if.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Un búfer de capacidad dos con variables de condición contiene un elemento. Cada uno de dos productores comprueba si hay espacio con el mutex adquirido, lo libera y luego vuelve a adquirirlo solo para put. Ambas comprobaciones terminan antes de cualquiera de las inserciones. ¿Qué estado pueden producir las dos inserciones?
  1. Count 2; la segunda comprobación reserva la posición.
  2. Count 1; una de las inserciones serializadas se descarta.
  3. Count 3; el intervalo permite que ambas comprobaciones admitan un elemento.
  4. Count 0; el índice que vuelve al inicio vacía el búfer.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un semáforo de finalización no tiene ningún hilo en espera cuando su trabajador señaliza. ¿Qué deja esa señalización para una espera posterior satisfactoria?
  1. Un propietario de mutex guardado.
  2. Una notificación perdida.
  3. Un hilo en espera bloqueado.
  4. Un permiso almacenado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un cerrojo de lectura protege una caché. Un lector ve que falta una entrada, libera el acceso de lectura y espera el acceso de escritura. Otro escritor rellena la entrada antes de que el lector entre. ¿Qué debe hacer el lector original después de adquirir el acceso de escritura?
  1. Insertar inmediatamente usando la observación anterior.
  2. Volver a comprobar si la entrada sigue faltando.
  3. Recuperar el acceso de lectura conservando el de escritura.
  4. Liberar el acceso de escritura sin examinar la caché.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

La inicialización y el primer uso adquieren cada uno el mismo mutex, pero el primer uso a veces encuentra un registro sin inicializar. ¿Qué garantía falta?
  1. Acceso exclusivo durante cada operación.
  2. Inicialización completada antes del uso.
  3. Un cerrojo privado distinto para cada operación.
  4. Un retraso fijo después de cada liberación del mutex.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Las tareas adquieren sus conjuntos completos de recursos mientras tienen adquirido un mutex global de adquisición y luego lo liberan antes de realizar el trabajo. Todas las rutas siguen esta regla y la liberación de recursos nunca necesita el mutex de adquisición. ¿Por qué las adquisiciones parciales que compiten no pueden formar un ciclo?
  1. Solo puede existir una secuencia de adquisición incompleta.
  2. Ningún hilo puede retener un recurso mientras espera.
  3. Todos los recursos pueden expropiarse al empezar el trabajo.
  4. La CPU no puede expropiar a un hilo durante la adquisición.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Hay 9 recursos idénticos. A tiene 3 con Max 7; B tiene 2 con Max 4. A solicita 3 más. Según el algoritmo del banquero, ¿debe concederse esa solicitud?
  1. No; el estado resultante no tiene una secuencia segura.
  2. Sí; B puede terminar antes que A.
  3. No; la solicitud supera la demanda restante de A.
  4. Sí; A puede terminar antes que B.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un socket de flujo disponible para lectura devuelve 0 en una lectura satisfactoria. ¿Qué condición de entrada debe gestionar el manejador?
  1. Un resultado temporal que indica que habría que esperar.
  2. Un byte recibido con valor cero.
  3. El fin de la entrada desde el otro extremo.
  4. Una finalización pendiente de lectura de disco.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un manejador pone en cola una lectura asíncrona de disco, libera su búfer de destino y retorna. La solicitud sigue pendiente. ¿Qué debe cambiar para que la ruta de finalización pueda usar el resultado de forma segura?
  1. Conservar solo el valor del descriptor hasta el retorno.
  2. Leer de inmediato porque el envío ya tuvo éxito.
  3. Liberar el búfer antes de enviar la solicitud.
  4. Mantener válido el búfer hasta que termine la entrada/salida.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Un bucle comprueba una cuota compartida, espera una aprobación externa y luego decrementa la cuota. Dos tareas pueden superar la comprobación antes de que cualquiera se reanude. ¿Qué cambio impide que ambas reclamen la última unidad?
  1. Hacer atómicas las cargas de la cuota, pero mantener dividida la decisión.
  2. Proteger solo el decremento final después de recibir la aprobación.
  3. Reservar una unidad de cuota de forma atómica antes de esperar.
  4. Reservar una unidad después de la aprobación usando la comprobación anterior.
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

Un hilo que espera una condición tiene adquirido el mutex M para su predicado y también el cerrojo L. La espera libera M. El único hilo que puede hacer verdadero el predicado debe adquirir primero L. ¿Qué impide el progreso y qué dependencia debe cambiar?
  1. El hilo que notifica debe adquirir M dos veces antes de cambiar el estado.
  2. El hilo en espera debe mantener M adquirido durante toda la suspensión.
  3. El hilo en espera debe descartar el predicado y esperar una vez.
  4. El hilo en espera debe dejar de retener L durante esa espera.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Cuatro consumidores procesan una cola FIFO. Ya se ha esperado a que terminen todos los productores, pueden quedar elementos reales en cola y un marcador de parada reservado hace que un consumidor salga. ¿Cuántos marcadores debe poner en cola el coordinador?
  1. 1
  2. 4
  3. 3
  4. 5
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un conjunto comienza con tres permisos. Un usuario ejecuta una espera satisfactoria, devuelve después su recurso y señaliza dos veces por accidente. Cuando todo el trabajo está inactivo, ¿cuántos usuarios pueden superar sus esperas antes de que alguien vuelva a señalizar?
  1. 4
  2. 2
  3. 3
  4. 5
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un búfer con semáforos de capacidad dos está vacío. P1 y P2 ejecutan cada uno una espera satisfactoria en empty; luego P1 inserta con el mutex adquirido y lo libera, pero aún no ha señalizado full. P2 no ha insertado. Un consumidor espera ahora en full. ¿Qué ocurre?
  1. Extrae el elemento de P1 antes de que P2 inserte.
  2. Se bloquea hasta que P2 complete su inserción.
  3. Se bloquea hasta que se publique un permiso de elemento.
  4. Extrae el elemento de P1 cuando el mutex queda libre.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un programador quiere que el incremento de un contador compartido sea una actualización indivisible sin usar un mutex. ¿Qué operación atómica implementa directamente esa actualización?
  1. Solo una carga atómica.
  2. Solo un almacenamiento atómico.
  3. Una notificación de condición.
  4. Obtener y sumar.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Cinco filósofos comensales usan un semáforo inicializado en 4. Cada uno espera en él, toma un tenedor izquierdo, señaliza el semáforo y luego intenta tomar el tenedor derecho. ¿Pueden los cinco quedar bloqueados con un tenedor cada uno?
  1. No; el recuento inicial limita permanentemente a cuatro a quienes tienen tenedores.
  2. Sí; devolver los permisos pronto permite que los cinco lleguen al ciclo de tenedores.
  3. No; cada señalización devuelve atómicamente el tenedor izquierdo del filósofo.
  4. Sí; pero solo si el recuento inicial del semáforo supera cuatro.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Available es (1, 1). P tiene (1, 0), Max (3, 1); Q tiene (1, 1), Max (2, 2). P solicita (0, 1). ¿Debe el algoritmo del banquero conceder esta solicitud?
  1. Sí; P puede terminar con los recursos restantes.
  2. No; el estado provisional no tiene una secuencia segura.
  3. Sí; Q puede terminar con los recursos restantes.
  4. No; P solicita más que su demanda restante.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un detector con varias instancias observa Available (0, 0). P tiene (1, 0) y solicita (0, 1); Q tiene (0, 1) y solicita (1, 0). R no tiene nada y solicita (1, 0). ¿Qué hilos que retienen recursos sostienen el interbloqueo?
  1. P, Q y R.
  2. P y R.
  3. P y Q.
  4. Q y R.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un bucle select vigila inicialmente los sockets de flujo 6 y 10. Solo se devuelve el 6 como disponible. El bucle reutiliza ese conjunto devuelto sin reconstruirlo, mientras el 10 recibe datos y las lecturas en el 6 devuelven ahora 0. ¿Qué dos fallos pueden producirse?
  1. El socket 10 sigue vigilado; el 6 entrega un byte de datos con valor cero.
  2. El socket 10 queda disponible para escritura; el 6 espera una lectura de disco.
  3. Se omite el socket 10; el 6 puede volver a despacharse al llegar al fin de entrada.
  4. El socket 10 sustituye al 6 en el conjunto; select cierra el 6.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Hay cuarenta solicitudes en cola en un bucle de eventos. Cada manejador realiza una lectura bloqueante de disco de 6 ms antes de retornar, y el resto del trabajo es insignificante. ¿Cuánto espera la última solicitud antes de que comience su lectura?
  1. 234 ms
  2. 240 ms
  3. 6 ms
  4. 39 ms
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

Un búfer circular de capacidad dos comienza vacío. Inserta 31, inserta 32, extrae una vez e inserta 33. Ambos índices están ahora en 1. ¿Cuáles son el recuento de ocupación y el valor devuelto por la siguiente extracción?
  1. Recuento 1; siguiente valor 32.
  2. Recuento 2; siguiente valor 33.
  3. Recuento 0; la extracción debe esperar.
  4. Recuento 2; siguiente valor 32.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Un hilo de Java llama a notifyAll sobre un objeto fuera de cualquier región synchronized y sin ser propietario de su monitor por otro medio. ¿Qué ocurre?
  1. Guarda una notificación para los hilos que esperen después.
  2. Lanza IllegalMonitorStateException.
  3. Entrega los cerrojos del hilo que llama a los hilos en espera.
  4. Espera hasta que el monitor esté libre.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un semáforo se implementa con un mutex, una variable de condición y un valor no negativo. La señalización incrementa y notifica con el mutex adquirido. La espera comprueba el valor con if, espera si es cero y luego decrementa. ¿Cómo puede fallar aun así?
  1. El mutex permite que dos hilos cambien el valor simultáneamente.
  2. El semáforo no puede recordar ninguna señalización sin hilos en espera.
  3. Un hilo que retorna de la espera puede decrementar un valor que es cero.
  4. Una señalización siempre decrementa el valor si no hay hilos en espera.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un consumidor tiene adquirido el mutex de un búfer mientras espera en el semáforo de elementos. El búfer está vacío. Una corrección propuesta hace que el productor señalice el semáforo de elementos antes de insertar, para poder despertar al consumidor sin el mutex. ¿Por qué es insegura esa corrección?
  1. Permite que la extracción ocurra antes de la inserción del productor.
  2. Permite que la inserción ocurra antes de reservar espacio.
  3. Devuelve un permiso de posición libre antes de terminar la extracción.
  4. Publica dos veces el mismo permiso de elemento completado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

En el diseño básico con contador de lectores, el primer lector puede tener adquirido el cerrojo del contador mientras espera writelock. ¿Por qué puede terminar el escritor activo, a diferencia de un productor bloqueado por el mutex del búfer que tiene adquirido un consumidor?
  1. El lector en espera libera el cerrojo del contador en sem_wait.
  2. El propio escritor ya tiene adquirido el cerrojo del contador de lectores.
  3. La señalización del escritor espera a que el contador de lectores llegue a cero.
  4. El escritor puede señalizar writelock sin el cerrojo del contador.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Un trabajador lee un puntero compartido antes de que el creador haya terminado de asignarlo. ¿Qué tipo de error de dependencia es este?
  1. Una violación del orden de ejecución.
  2. Un interbloqueo de recursos.
  3. Inanición de los escritores.
  4. Un bloqueo activo por reintentos.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Dos bucles de actualización adquieren cada uno un cerrojo, fallan con trylock sobre el otro, lo liberan y reintentan juntos indefinidamente. Una corrección propuesta mantiene el primer cerrojo mientras espera normalmente el segundo. ¿Qué riesgo introduce ese cambio?
  1. Mantiene el bloqueo activo porque ambos hilos siguen reintentando.
  2. Garantiza que uno termine mientras el otro puede sufrir inanición.
  3. Sustituye el bloqueo activo por reintentos por un posible interbloqueo.
  4. Serializa ambas adquisiciones bajo un primer cerrojo común.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Available es (1, 1). P tiene (1, 0), Max (2, 1); Q tiene (0, 2), Max (1, 3); R tiene (1, 0), Max (3, 2). ¿Qué secuencia segura permite terminar a los tres?
  1. R, P, Q.
  2. P, R, Q.
  3. Q, R, P.
  4. P, Q, R.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Available es (0, 0). A tiene (1, 0), solicita actualmente (0, 0) y tiene Need máximo restante (0, 2). B tiene (0, 2) y solicita actualmente (1, 0). ¿Encuentra un interbloqueo la detección basada en solicitudes actuales?
  1. Sí; ningún Need máximo cabe en el Available inicial.
  2. No; A libera (1, 0), lo que permite atender la solicitud de B.
  3. Sí; la solicitud actual de B no puede atenderse de inmediato.
  4. No; ambas solicitudes ya caben en el Available inicial.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Un desarrollador sustituye los exámenes repetidos de solicitudes de entrada/salida pendientes por una cola de finalización. ¿Qué trabajo puede despachar ahora directamente el bucle?
  1. Las respuestas finales de cada solicitud recién enviada.
  2. El trabajo posterior de las operaciones notificadas como terminadas.
  3. Las lecturas de cada archivo regular que select indica como disponible.
  4. La ejecución paralela de todos los manejadores en un hilo de bucle.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

¿Te atascas con una pregunta tuya? Ayuda con Tareas

Términos Clave

90 Términos Clave, cada uno con su definición

A

Actualización perdida
Una actualización perdida ocurre cuando operaciones de lectura-modificación-escritura solapadas se sobrescriben entre sí, de modo que desaparece un cambio previsto.
Dos hilos cargan count 8 y cada uno almacena 9, aunque dos incrementos deberían dejar 10.
Admisión equitativa
La admisión equitativa concede acceso según una política que impide que una categoría continua de llegadas adelante indefinidamente a un hilo en espera.
Una política equitativa de lectores-escritores impide que los lectores posteriores adelanten sin fin a un escritor que llegó antes.
Algoritmo del banquero
El algoritmo del banquero concede una solicitud de recursos solo cuando cabe en las demandas restantes declaradas y preserva una secuencia segura de finalización.
Un asignador concede provisionalmente una solicitud, comprueba el estado resultante y retrasa una concesión insegura.
Ausencia de expropiación
La ausencia de expropiación significa que un recurso retenido no puede retirarse por la fuerza a quien lo tiene y debe liberarse mediante su protocolo.
Un hilo que espera un mutex ordinario no puede simplemente quitarle el cerrojo a su propietario actual.

B

Bloque de control de entrada/salida asíncrona
Un bloque de control de entrada/salida asíncrona describe una solicitud AIO de POSIX, incluidos su descriptor, desplazamiento en el archivo, búfer y número de bytes solicitados.
El aiocb y su búfer de destino permanecen válidos mientras la lectura en cola siga pendiente.
Bloqueo activo
Un bloqueo activo mantiene a los hilos ejecutándose y respondiendo unos a otros mientras sus acciones repetidas no consiguen completar trabajo útil.
Dos bucles con trylock liberan y reintentan repetidamente juntos sin que ninguno obtenga ambos cerrojos.
Bloqueo implícito
El bloqueo implícito retrasa un hilo mediante una operación que parece inmediata, como acceder a una página que debe leerse del disco.
Un manejador se detiene por un fallo de página aunque no realice ninguna llamada explícita de lectura bloqueante.
Bucle de eventos
Un bucle de eventos espera eventos y despacha manejadores breves, permitiendo que un hilo coordine muchas tareas cuyos periodos de espera se solapan.
Un servidor gestiona los datos de un socket disponible y luego vuelve a atender otra conexión.
Búfer acotado
Un búfer acotado es una cola compartida de capacidad fija que hace esperar a los productores cuando está llena y a los consumidores cuando está vacía.
Un búfer de capacidad tres no puede aceptar un cuarto elemento hasta que un consumidor extraiga uno.
Búfer circular
Un búfer circular almacena una cola en un arreglo cuyos índices de lectura y escritura vuelven de la última posición a la primera.
Después de escribir en la posición 2 de un arreglo de tres posiciones, la siguiente posición de escritura vuelve a ser la 0.

C

Cerrojo de lectores-escritores
Un cerrojo de lectores-escritores permite que varios lectores compartan el acceso o que un escritor tenga acceso exclusivo a los datos protegidos.
Varias consultas de caché pueden solaparse, pero actualizar la caché requiere acceso de escritura.
Cola de finalización
Una cola de finalización entrega registros de operaciones asíncronas terminadas, lo que permite a un programa recoger resultados y despachar el trabajo posterior.
El bucle lee un registro de finalización en vez de comprobar repetidamente cada lectura pendiente.
Comparar e intercambiar
Comparar e intercambiar sustituye atómicamente el valor de una ubicación solo cuando su valor actual coincide con el valor esperado proporcionado por quien llama.
Quien actualiza un contador reintenta cuando otro hilo cambia el valor anterior antes de que su CAS tenga éxito.
Comprobar y actuar
Una operación de comprobar y actuar comprueba el estado compartido y luego lo cambia o lo usa de una forma que depende de que esa comprobación siga siendo válida.
Comprobar las existencias y reservarlas debe permanecer unido cuando otros hilos pueden vender el mismo artículo.
Concurrencia híbrida
La concurrencia híbrida combina un bucle de eventos con trabajadores que realizan operaciones bloqueantes o que consumen mucha CPU fuera del hilo del bucle.
Un bucle de red envía el procesamiento de imágenes a trabajadores y después gestiona sus finalizaciones.
Condición de cobertura
Una condición de cobertura usa una notificación amplia para llegar a todos los hilos en espera cuya condición más específica podría cumplirse ahora.
Cuando un asignador con solicitantes de 10 bytes y de 100 bytes en espera libera 50 bytes, despierta a ambos para que cada uno vuelva a comprobar.
Conjunto de descriptores
Un conjunto de descriptores representa números de descriptores de archivo seleccionados para su vigilancia, con bits que indican su pertenencia o la disponibilidad devuelta.
El servidor reconstruye su conjunto de lectura porque select() lo sustituye por el subconjunto disponible.
Continuación
Una continuación almacena el estado necesario para reanudar una tarea después de que un manejador haya retornado mientras espera una operación asíncrona.
La solicitud de lectura guarda qué socket de cliente debe recibir sus datos.
Control de admisión
El control de admisión decide si puede entrar más trabajo en una región con recursos limitados y lo retrasa cuando la capacidad permitida está en uso.
Un trabajador espera un permiso de procesamiento antes de asignar su gran búfer temporal.
Conversión de cerrojo de lectura a escritura
Una conversión de cerrojo de lectura a escritura cambia el acceso de lectura por acceso exclusivo de escritura; en el diseño básico, primero debe liberarse el acceso de lectura.
Después de adquirir el acceso de escritura, quien actualiza la caché vuelve a comprobar si la entrada aún necesita modificarse.

D

Despertar espurio
Un despertar espurio es un retorno de una espera en una condición sin una notificación correspondiente, que requiere volver a comprobar el predicado como en cualquier despertar.
Un consumidor vuelve a esperar si despierta pero sigue encontrando el búfer vacío.
Despertar perdido
Un despertar perdido ocurre cuando una notificación necesaria pasa antes de que un hilo empiece a esperar, dejándolo suspendido sin una notificación futura.
Cuando un padre comprueba un indicador de finalización fuera del mutex, el hijo puede terminar entre la comprobación y la espera.
Detección de interbloqueos
La detección de interbloqueos busca en las dependencias de espera o solicitudes de recursos actuales un conjunto existente que no puede avanzar.
Una búsqueda de ciclos encuentra a tres hilos que tienen mutex adquiridos y están bloqueados unos por otros.
Disponibilidad para entrada/salida
La disponibilidad para entrada/salida indica que se puede intentar una operación de entrada/salida para un descriptor; no transfiere los datos al programa.
Un socket disponible para lectura puede devolver datos o indicar el fin de la entrada al leerlo.

E

Entrada/salida asíncrona
La entrada/salida asíncrona separa el envío de una operación de la recogida de su resultado una vez terminada, lo que permite a quien llama atender otro trabajo mientras tanto.
El manejador envía una lectura de archivo y envía la respuesta después de que llegue su finalización.
Entrada/salida no bloqueante
La entrada/salida no bloqueante devuelve el control cuando una operación no puede continuar inmediatamente, de modo que el hilo que llama puede ocuparse de otro trabajo.
Una lectura de socket devuelve EAGAIN y el manejador retorna al bucle de eventos.
epoll
epoll es una interfaz de eventos de Linux que mantiene un conjunto de interés en el núcleo e informa de los eventos disponibles para los descriptores registrados.
Un servidor puede vigilar muchas conexiones sin volver a enviar la lista completa de descriptores vigilados cada vez.
Espera a la terminación de un hilo
Una espera a la terminación de un hilo espera a que termine un hilo concreto antes de que el hilo que espera continúe con el trabajo que necesita esa finalización.
El hilo que genera informes espera a que termine el trabajador que suma antes de leer su total final.
Espera aleatoria antes del reintento
La espera aleatoria antes del reintento añade un retraso elegido al azar antes de volver a intentarlo, reduciendo las colisiones repetidas entre competidores que, de otro modo, actuarían al mismo ritmo.
Las transferencias que reintentan esperan intervalos diferentes para que sea menos probable que vuelvan a colisionar.
Espera circular
Una espera circular es una cadena cerrada de hilos en la que cada uno espera un recurso que tiene el siguiente hilo de la cadena.
Tres hilos que esperan de A a B, de B a C y de C a A cierran el ciclo de dependencias.
Estado inseguro
Un estado inseguro no tiene una secuencia de finalización garantizada para las demandas máximas, aunque el comportamiento futuro real aún puede evitar el interbloqueo.
Un hilo podría liberar recursos antes de tiempo en vez de solicitar toda su demanda restante.
Estado seguro
Un estado seguro tiene al menos un orden de finalización que puede satisfacer la demanda máxima restante de cada hilo usando los recursos disponibles y los liberados.
Un hilo puede terminar primero y liberar suficientes recursos para que un segundo hilo termine a continuación.
Evitación de interbloqueos
La evitación de interbloqueos comprueba las concesiones de recursos propuestas con información previa sobre la demanda y retrasa las concesiones que dejarían el sistema en un estado inseguro.
Un recurso libre puede no concederse cuando hacerlo eliminaría todas las secuencias seguras de finalización.
Exclusión mutua
La exclusión mutua permite que solo un hilo a la vez tenga un recurso o ejecute una operación protegida sobre el estado compartido.
Un mutex exclusivo no puede concederse a un segundo hilo mientras su propietario aún lo tenga adquirido.

G

Gestión manual de la pila
La gestión manual de la pila conserva explícitamente el estado local de una tarea entre manejadores que retornan, en vez de depender de una sola pila de llamadas suspendida.
Un registro de respuesta conserva la posición y el destino de la solicitud hasta que termina la lectura.
Grafo de espera
Un grafo de espera representa los hilos como nodos y las dependencias de espera como aristas dirigidas desde el hilo en espera hacia quien retiene un recurso.
Una flecha de T1 a T2 significa que T1 necesita un recurso que T2 tiene actualmente.
Grupo de trabajadores
Un grupo de trabajadores es un conjunto acotado de hilos o procesos reutilizables que realizan las tareas enviadas y devuelven sus resultados.
libuv despacha las operaciones del sistema de archivos a su grupo de hilos mientras el bucle atiende eventos.

I

Inanición
La inanición aplaza indefinidamente a un hilo mientras otros continúan realizando trabajo útil, aunque ningún ciclo de dependencias bloqueadas los atrape a todos.
Un escritor sigue esperando mientras un flujo ininterrumpido de lectores cuyas lecturas se solapan usa el recurso.
Inanición de los escritores
La inanición de los escritores es el aplazamiento indefinido de un escritor en espera mientras los lectores siguen entrando y avanzando.
Un cerrojo que solo cuenta lectores no da turno a un escritor si continuamente se solapan nuevos lectores.
Indicador de disponibilidad
Un indicador de disponibilidad registra bajo sincronización que la inicialización ha terminado, permitiendo que quienes usan el estado esperen hasta que exista.
El creador establece ready solo después de asignar el registro que usará el trabajador.
Intento de adquisición sin espera
Un intento de adquisición sin espera intenta adquirir un mutex y retorna inmediatamente cuando no está disponible, permitiendo que el hilo que llama libere y reintente.
Un segundo trylock fallido hace que la transferencia libere el primer cerrojo de cuenta.
Interbloqueo
Un interbloqueo atrapa a un conjunto de hilos bloqueados porque cada uno necesita una acción o recurso que solo otro miembro atrapado puede proporcionar.
T1 tiene A y espera B mientras T2 tiene B y espera A.
Intercalado
Un intercalado es un orden de pasos de distintos hilos que conserva el orden propio de cada hilo mientras mezcla sus accesos.
Situar la anulación de un puntero entre la comprobación y el uso de otro hilo revela la planificación insegura.
io_uring
io_uring es una interfaz de Linux con colas de envío y finalización para operaciones asíncronas, que permite gestionar la entrada/salida según las finalizaciones.
Un servidor envía solicitudes y más tarde despacha continuaciones a partir de sus registros de finalización.

K

kqueue
kqueue es una interfaz de notificación de eventos de FreeBSD y macOS que mantiene los intereses registrados y devuelve los eventos notificados.
Un servidor registra intereses sobre sockets con kqueue en vez de reconstruir conjuntos completos de descriptores de select.

L

Liberación y espera atómicas
La liberación y espera atómicas convierten la liberación del mutex y el inicio de una espera en una condición en una única transición indivisible frente a los hilos que compiten.
El hijo no puede notificar en un intervalo después de que el padre libere el mutex pero antes de que empiece a esperar.
Limitación de la concurrencia
La limitación de la concurrencia limita la entrada simultánea a una fase costosa para que la demanda conjunta permanezca dentro de un presupuesto de recursos elegido.
Doce trabajadores simultáneos de 500 MB usan 6000 MB durante su fase de alto consumo de memoria.

M

Marcador de fin de datos
Un marcador de fin de datos es un elemento reservado de la cola que indica a un consumidor que se detenga después de procesar todos los elementos reales anteriores.
Un coordinador espera a que termine cada productor antes de insertar un marcador de parada por consumidor.
Modelo de contador con signo
El modelo de contador con signo representa mediante valores negativos a los hilos bloqueados en espera de un semáforo, manteniendo el mismo comportamiento de espera y señalización que observan los hilos.
Un valor de seguimiento de −2 representa dos hilos bloqueados en espera, no un recuento negativo de permisos en Linux.
Monitor
Un monitor agrupa el estado compartido con operaciones que admiten un hilo a la vez y con esperas basadas en condiciones.
Un bloque synchronized de Java mantiene adquirido el monitor del objeto necesario para llamar a wait().
Mutex del búfer
Un mutex del búfer serializa los cambios en el arreglo y los índices del búfer, independientemente de los semáforos que cuentan los recursos disponibles.
Dos productores con permisos de espacio válidos siguen actualizando el índice de escritura del búfer, fill_ptr, bajo el mismo mutex.

N

Notificación a todos los hilos en espera
Una notificación a todos los hilos en espera despierta a todos los que esperan en una variable de condición; cada uno debe volver a adquirir el mutex y comprobar su propio predicado.
Un asignador notifica a todos después de una liberación porque los hilos en espera solicitan distintas cantidades de memoria.
Notificación a un hilo en espera
Una notificación a un hilo en espera avisa al menos a un hilo que espera en una variable de condición, si los hay, sin almacenar una notificación para quienes lleguen después.
Un hijo notifica después de establecer su indicador protegido de finalización para que un padre en espera pueda volver a comprobarlo.

O

Obtener y sumar
Obtener y sumar es una operación atómica de lectura-modificación-escritura que añade una cantidad y devuelve el valor anterior sin una actualización intermedia.
Una operación atómica de obtener y sumar evita que los incrementos concurrentes de un contador se sobrescriban entre sí.
Operación de espera del semáforo
Una operación de espera del semáforo adquiere un permiso de forma atómica y se bloquea cuando no hay ninguno disponible hasta que otra operación proporcione uno.
Un usuario espera antes de tomar un identificador para no superar el límite de admisión del conjunto.
Operación de señalización del semáforo
Una operación de señalización del semáforo proporciona un permiso de forma atómica, lo que permite continuar a un hilo bloqueado o guarda disponibilidad para una espera posterior.
Un trabajador señaliza su semáforo de finalización aunque su padre aún no haya empezado a esperar.
Orden de adquisición de cerrojos
El orden de adquisición de cerrojos asigna un orden compartido a los cerrojos y exige que todas las rutas de código pertinentes lo sigan.
Ambas direcciones de una transferencia adquieren el cerrojo de la cuenta de número menor antes que el de la cuenta de número mayor.

P

Paralelismo multinúcleo
El paralelismo multinúcleo ejecuta cálculos en varios núcleos de CPU al mismo tiempo y requiere más trabajadores de ejecución que un solo hilo de bucle.
Varios trabajadores de procesamiento de imágenes pueden calcular a la vez mientras un bucle de eventos gestiona sockets.
Permiso
Un permiso representa una unidad disponible de admisión o de recurso que una espera satisfactoria en un semáforo consume y una señalización proporciona.
Un semáforo con tres permisos sin usar puede admitir a tres hilos sin bloquearlos.
poll
poll() vigila un arreglo de entradas de descriptores para detectar los eventos de entrada/salida solicitados sin la restricción fija de valor de descriptor de fd_set.
Un socket de número alto puede vigilarse con poll incluso cuando su valor no cabe en un conjunto de select.
Predicado
Un predicado es una condición verdadera o falsa sobre el estado compartido que un hilo comprueba antes de decidir si puede continuar.
El predicado done != 0 permite que un padre compruebe si su hijo ha terminado.
Preferencia a los escritores
La preferencia a los escritores bloquea a los nuevos lectores cuando hay un escritor esperando, lo que reduce el aplazamiento de los escritores, pero puede causar inanición a los lectores.
Una consulta que llega espera detrás del escritor de la caché en vez de unirse al grupo actual de lectores.
Preferencia a los lectores
La preferencia a los lectores permite que los lectores que llegan se unan a un grupo de lectores activo incluso cuando un escritor ya está esperando acceso.
Los lectores cuyas lecturas se solapan pueden mantener el contador de lectores por encima de cero mientras un escritor espera.
Prevención de interbloqueos
La prevención de interbloqueos diseña la adquisición de recursos para que al menos una condición necesaria del interbloqueo nunca pueda surgir.
Un orden común de cerrojos de cuentas impide que las transferencias en direcciones opuestas creen una espera circular.
Problema del productor-consumidor
El problema del productor-consumidor coordina hilos que añaden y extraen elementos de un búfer compartido de capacidad limitada.
Un receptor de red produce solicitudes que los hilos trabajadores consumen de una cola acotada.
Propiedad del semáforo
Un semáforo no tiene propietario, por lo que cualquier hilo puede señalizar un permiso, incluso cuando otro hilo realizó la espera correspondiente.
Un hijo señaliza un semáforo de finalización en el que espera su padre.
Punto de encuentro
Un punto de encuentro es un lugar que ninguno de dos hilos puede superar hasta que ambos hayan anunciado que llegaron.
Cada hilo señaliza su llegada antes de esperar el permiso de llegada del otro hilo.

R

Reserva de recursos
Una reserva de recursos asigna una unidad de recurso a una operación admitida, incluso antes de que esa operación actualice los datos de control compartidos.
La espera satisfactoria de un productor por espacio reserva una posición mientras espera el mutex del búfer.
Retención y espera
La retención y espera ocurre cuando un hilo conserva los recursos que ya tiene mientras se bloquea esperando recursos adicionales que necesita.
Una transferencia conserva un cerrojo de cuenta mientras espera adquirir el cerrojo de la otra cuenta.
Reversión
Una reversión restaura un punto de control válido anterior para que el trabajo pueda liberar o recuperar recursos sin conservar cambios parciales.
Un sistema de transacciones aborta y reintenta una transacción interbloqueada mediante su mecanismo de reversión.

S

Secuencia segura
Una secuencia segura ordena todos los hilos de modo que cada demanda máxima restante quepa en los recursos disponibles después de que terminen los hilos anteriores.
Cada paso de una secuencia propuesta debe caber en cada tipo de recurso, no solo en el recuento total.
select
select() vigila conjuntos de descriptores para detectar su disponibilidad para entrada/salida, los sustituye por los subconjuntos disponibles y limita el examen a los números de descriptor inferiores a nfds.
Vigilar los descriptores 5 y 12 requiere nfds 13 y reconstruir los conjuntos vigilados después de cada llamada.
Semáforo
Un semáforo es un objeto de sincronización basado en un entero con operaciones atómicas de espera y señalización que adquieren y proporcionan permisos.
Una señalización puede ocurrir antes de una espera porque el semáforo recuerda el permiso no utilizado.
Semáforo binario
Un semáforo binario usado como cerrojo comienza con un permiso y combina su adquisición antes de una sección crítica con su liberación después.
Un segundo hilo que intenta entrar se bloquea hasta que el primero devuelve el permiso de la sección crítica.
Semáforo contador
Un semáforo contador comienza con el número de unidades idénticas disponibles y limita cuántos usuarios pueden adquirirlas a la vez.
Un conjunto de conexiones con cuatro permisos hace que un quinto usuario espere a que se devuelva una conexión.
Semáforo de elementos disponibles
Un semáforo de elementos disponibles cuenta los elementos publicados del búfer que los consumidores pueden reservar antes de extraerlos del arreglo compartido.
Un búfer inicialmente vacío da cero permisos a su semáforo de elementos disponibles.
Semáforo de espacios libres
Un semáforo de espacios libres cuenta los permisos para reservar posiciones disponibles del búfer antes de que los productores inserten sus elementos completados.
Un búfer vacío de capacidad cinco inicializa su semáforo de espacios libres en cinco.
Semántica de Hoare
La semántica de Hoare transfiere el cerrojo y la ejecución directamente a un hilo notificado, preservando la condición notificada en el momento de la entrega.
El hilo notificado se reanuda antes de que otro hilo que entra pueda consumir el elemento recién disponible.
Semántica de Mesa
La semántica de Mesa deja listo a un hilo notificado para competir por su cerrojo, por lo que debe volver a comprobar la condición después de adquirirlo de nuevo.
Un segundo consumidor puede tomar el único elemento antes de que el primer consumidor notificado recupere el mutex.
Suspensión
Una suspensión pausa una función hasta que termine una operación esperada, mientras permite que el bucle de eventos ejecute otro trabajo disponible.
Una función async se reanuda después de que termine su lectura, con sus variables locales conservadas.

T

Temporizador de vigilancia
Un temporizador de vigilancia inicia la recuperación cuando el software no informa de su progreso dentro de un plazo, sin diagnosticar la causa de forma inequívoca.
Un controlador embebido se reinicia después de que su software deje de enviar los informes de progreso esperados.
Tiempo de espera agotado
Un tiempo de espera agotado termina una espera después de un plazo y revela un retraso excesivo sin distinguir un interbloqueo permanente de un progreso lento.
Una espera con plazo agotado para un mutex puede deberse tanto a un sistema sobrecargado como a un ciclo de cerrojos.

V

Variable de condición
Una variable de condición proporciona una cola en la que los hilos se suspenden hasta que un cambio en el estado compartido les permite volver a comprobar su condición.
Un consumidor espera en una variable de condición cuando la cola de solicitudes no contiene trabajo.
Variable de estado
Una variable de estado registra un hecho sobre el trabajo compartido, de modo que un hilo puede comprobarlo incluso después de que haya pasado una notificación anterior.
Un indicador de finalización permanece establecido cuando el hijo termina antes de que el padre empiece a esperar.
Vector de necesidades
Un vector de necesidades registra la demanda máxima restante de un hilo para cada tipo de recurso, calculada como Max menos la Allocation actual.
Un hilo con Max (4, 3) y Allocation (1, 2) tiene Need (3, 1).
Vector de recursos disponibles
Un vector de recursos disponibles cuenta las unidades actualmente no asignadas de cada tipo de recurso y forma el valor inicial de Work para una comprobación de seguridad.
Resta todas las unidades retenidas de cada total de recursos antes de comenzar la simulación de finalización.
Vector de trabajo
Un vector de trabajo es la disponibilidad simulada de la comprobación de seguridad, que aumenta en la Allocation de un hilo cada vez que este se marca como terminado.
La finalización de un hilo que tiene (2, 1) cambia Work de (1, 3) a (3, 4).
Víctima seleccionada para la recuperación
Una víctima seleccionada para la recuperación es trabajo que un sistema elige para abortar o revertir con el fin de liberar recursos y romper un interbloqueo detectado.
Un proceso con poco trabajo completado puede ser más barato de reiniciar que uno casi terminado.
Violación de atomicidad
Una violación de atomicidad permite que otro hilo interrumpa accesos que deben comportarse como una sola operación indivisible.
Dos regiones con cerrojo separadas alrededor de una comprobación de saldo y un débito siguen dejando un intervalo inseguro.
Violación del orden de ejecución
Una violación del orden de ejecución ocurre cuando una operación se ejecuta antes que otra operación anterior necesaria porque su dependencia no está garantizada.
Un trabajador lee la configuración antes de que el hilo de carga haya terminado de publicar los valores.

Sobre esta unidad

Por StudeliInformar de un error

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.

Comenzar