Hilos, condiciones de carrera y cerrojos

Aprende cómo los hilos permiten que un programa haga varias cosas a la vez y por qué compartir datos entre ellos es arriesgado. Crea hilos y espera a su terminación con hilos POSIX en C, sigue cómo las condiciones de carrera provocan actualizaciones perdidas y protege las secciones críticas con mutex. Descubre cómo se construyen cerrojos a partir de instrucciones atómicas de hardware como comprobar y establecer y comparar e intercambiar, por qué los sistemas operativos suspenden los hilos en espera y cómo diseñar contadores, listas, colas y tablas hash que sigan siendo correctos y rápidos con muchos hilos.

IntermedioUnidad 5 de 9 del curso Curso de Sistemas operativos

Lo que aprenderás

  1. Describe cómo los hilos de un proceso comparten un espacio de direcciones mientras cada uno conserva su propio estado de ejecución, y escribe programas en C que creen hilos, les pasen datos y esperen a que terminen.
    • Compara los hilos con los procesos
      Explica qué posee un hilo, qué comparte con los demás hilos de su proceso, por qué los programas usan hilos y en qué se diferencia un cambio entre hilos de un cambio entre procesos.
    • Crea hilos, pásales datos y espera a su terminación con pthreads
      Usa la API de hilos POSIX en C para iniciar hilos, dar a cada uno sus argumentos, recoger sus resultados y asegurar que el programa espere a que terminen antes de finalizar.
  2. Sigue cómo las instrucciones máquina intercaladas sobre datos compartidos producen resultados incorrectos, identifica condiciones de carrera y secciones críticas en el código y enuncia qué debe garantizar una solución correcta para las secciones críticas.
    • Sigue cómo las actualizaciones sin protección pierden incrementos
      Descompón una actualización compartida en sus pasos de máquina, sigue los intercalados que pierden actualizaciones y explica por qué estos errores son poco frecuentes en las pruebas, aunque son reales.
    • Identifica condiciones de carrera, secciones críticas y operaciones atómicas
      Define el vocabulario esencial de la concurrencia, reconoce en el código las carreras de lectura-modificación-escritura y de comprobar y actuar, y enuncia los requisitos que debe cumplir una solución correcta al problema de la sección crítica.
  3. Usa mutex POSIX para hacer atómicas las secciones críticas, decide qué protege cada cerrojo y cuánto código abarca, y evita los errores que anulan silenciosamente la protección.
    • Adquiere y libera un mutex POSIX al entrar y salir de las secciones críticas
      Explica qué garantiza un cerrojo, inicializa y usa correctamente un mutex de pthread y corrige con él las carreras del contador compartido y de la cuenta bancaria.
    • Elige la granularidad de los cerrojos y evalúa su funcionamiento
      Decide cuántos datos protege cada cerrojo y cuánto código abarca, y evalúa un cerrojo por su corrección, equidad y rendimiento.
    • Evita errores habituales al usar cerrojos
      Reconoce los errores típicos que dejan datos sin protección o hilos esperando para siempre, y corrige cada uno.
  4. Muestra por qué las cargas y los almacenamientos ordinarios no bastan para un cerrojo correcto en el hardware moderno, construye cerrojos de espera activa con instrucciones atómicas, garantiza su equidad y explica cómo el soporte del sistema operativo evita el gasto de CPU de la espera activa.
    • Explica por qué fallan los intentos sencillos de implementar cerrojos
      Analiza tres primeros enfoques para la exclusión mutua, la desactivación de interrupciones, un indicador ordinario y el algoritmo de Peterson, y explica dónde falla cada uno.
    • Construye un cerrojo de espera activa con instrucciones atómicas
      Describe comprobar y establecer, comparar e intercambiar, carga enlazada y almacenamiento condicional, y obtener y sumar; construye un cerrojo de espera activa a partir de ellas y evalúalo.
    • Garantiza la equidad de la espera activa con un cerrojo por boletos
      Construye un cerrojo por boletos con obtener y sumar, sigue su funcionamiento y explica cómo garantiza que cada hilo en espera reciba su turno.
    • Evita el gasto de CPU de la espera activa mediante cesión de la CPU, suspensión y cerrojos de dos fases
      Explica cómo la cesión de la CPU, la suspensión en una cola, los futexes y los cerrojos de dos fases reducen el tiempo de CPU desperdiciado, decide cuándo merece la pena la espera activa y reconoce la inversión de prioridades.
  5. Haz que contadores, listas enlazadas, colas y tablas hash sean seguros para muchos hilos mediante cerrojos, y evalúa cada diseño por su corrección y por su escalabilidad al añadir hilos.
    • Construye contadores escalables
      Protege un contador con un cerrojo, explica por qué escala mal y construye un contador aproximado con contadores por núcleo y un umbral.
    • Haz que las listas enlazadas sean seguras entre hilos
      Protege una lista enlazada con un cerrojo, mantén pequeñas las secciones críticas y libera el cerrojo en todas las rutas, y evalúa cuándo ayuda usar un cerrojo por nodo.
    • Diseña colas y tablas hash concurrentes
      Construye una cola con dos cerrojos y un nodo ficticio y una tabla hash con un cerrojo por cubeta, y sigue un enfoque basado en mediciones para usar cerrojos de grano más fino.

Guía de estudio de Hilos y cerrojos

5 capítulos

1

Los hilos y su memoria compartida

Un proceso proporciona a un programa un espacio de direcciones; los hilos le dan varias rutas de ejecución dentro de ese espacio. Empieza por separar el estado que lleva cada hilo de los objetos a los que todos pueden acceder. Después usa la interfaz de hilos POSIX para iniciar hilos trabajadores y recoger sus resultados. Las preguntas principales de diseño son quién posee cada argumento, cuánto tiempo permanece viva su memoria y cuándo el hilo que los creó puede leer la respuesta de forma segura.

1.1 Un espacio de direcciones, varias rutas de ejecución

Un hilo es una secuencia de ejecución dentro de un proceso. Varios hilos pueden trabajar con los mismos datos del programa, pero cada uno necesita su propia posición en el código y sus propias llamadas a funciones aún sin terminar.

Qué se comparte y qué pertenece a un hilo

Los hilos de un proceso comparten código, variables globales y estáticas, objetos del montículo, descriptores de archivo abiertos y el identificador de proceso. Cada hilo tiene su propio contador de programa, registros, pila e identificador de hilo. Una variable global int total designa un único objeto compartido. Una variable local ordinaria int i en la función trabajadora designa un objeto distinto en cada llamada.

Un proceso con tres hilos tiene tres pilas en direcciones distintas del mismo espacio de direcciones. Una pila no es un compartimento protegido: otro hilo puede acceder a una variable local si recibe su dirección. Esa compartición solo es válida mientras la variable existe. Un puntero a un marco de pila de una función que ha terminado no puede mantener viva la variable. Los arreglos locales grandes y la recursión profunda también pueden agotar la pila de un hilo.

Cambios entre hilos

El sistema operativo guarda el estado de ejecución de cada hilo en un bloque de control de hilo, o TCB. Cuando cambia de T1 a T2, guarda los registros de T1 y restaura los de T2. El cálculo sin terminar de T1 permanece en sus registros guardados hasta que vuelva a ejecutarse.

Para los hilos del mismo proceso, el espacio de direcciones sigue siendo el mismo durante el cambio. No es necesario cambiar las tablas de páginas, lo que normalmente hace que este cambio cueste menos que cambiar entre procesos. La memoria compartida también implica fallos compartidos: una escritura mediante un puntero no válido puede dañar los datos de otro hilo, y un fallo que termine el proceso acaba con todos los hilos.

¿Por qué usar varios hilos?

El paralelismo significa ejecutar en el mismo instante en distintos núcleos de CPU. Cuatro partes independientes de un cálculo pueden acercarse a una aceleración de cuatro veces en cuatro núcleos, si el trabajo compartido y la coordinación no se convierten en cuellos de botella. Los hilos también permiten que un programa solape la espera con el trabajo: mientras uno espera una lectura de disco o una respuesta de red, otro puede usar la CPU.

La concurrencia significa que las ejecuciones avanzan durante periodos que se solapan. En un núcleo, el planificador cambia entre hilos, por lo que son concurrentes sin ejecutarse en paralelo. Un solo núcleo sigue permitiendo errores de concurrencia: un cambio puede interrumpir una actualización compartida entre sus instrucciones máquina.

¿Quién planifica los hilos?

Linux y Windows usan un modelo uno a uno: cada hilo ordinario de una aplicación corresponde a un hilo planificado por el núcleo. Distintos hilos pueden ejecutarse en distintos núcleos, y una llamada bloqueante bloquea al hilo que la realiza. Una biblioteca muchos a uno planifica varios hilos de usuario sobre un hilo del núcleo. Sus cambios pueden costar poco, pero no puede ejecutar esos hilos en varios núcleos, y una llamada al sistema bloqueante puede detener a todo el grupo.

Crear A antes que B no garantiza que A se ejecute primero. Si A imprime «A» y B imprime «B», puede aparecer «AB» o «BA». El orden de creación establece cuándo un hilo pasa a estar disponible para ejecutarse; el planificador elige su momento de ejecución.

Puntos clave

  • Los hilos comparten el espacio de direcciones del proceso, pero cada uno tiene sus propios registros, contador de programa y pila.
  • Un puntero puede hacer que una variable de pila sea compartida; su tiempo de vida sigue terminando con la función a la que pertenece.
  • La concurrencia es posible en un núcleo; la ejecución en paralelo necesita varios núcleos.
  • El planificador elige qué hilo listo para ejecutarse se ejecuta a continuación.
  • El modelo de hilos uno a uno permite la ejecución en paralelo y el bloqueo de cada hilo por separado.

1.2 Inicia hilos trabajadores y recoge sus resultados

La interfaz de hilos POSIX, o pthreads, inicia una función de C en otro hilo. Incluye <pthread.h> y compila con un comando como gcc -pthread -o sum sum.c. La opción -pthread proporciona la configuración del compilador y del enlazador para código con hilos.

Crea un hilo con argumentos estables

La llamada pthread_create(&t, NULL, worker, arg) guarda un identificador de hilo en t e inicia worker(arg). El puntero de atributos NULL solicita los valores predeterminados. Una función trabajadora tiene la firma void *worker(void *arg): el argumento y el valor de retorno son punteros. Una estructura permite que ese único puntero transporte varios campos.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

struct range { int lo, hi; long result; };

void *sum_range(void *arg) {
    struct range *r = arg;
    long sum = 0;
    for (int n = r->lo; n <= r->hi; n++)
        sum += n;
    r->result = sum;
    return NULL;
}

El hilo creador prepara dos estructuras, {1, 50, 0} y {51, 100, 0}, y pasa una dirección distinta a cada hilo trabajador. Mantén ambas estructuras en memoria hasta que los trabajadores terminen. Cada trabajador acumula en su propia variable local sum y escribe solo en su propio campo result.

pthread_create devuelve 0 si tiene éxito y un número de error si falla. Comprueba directamente ese número devuelto; no establece errno para comunicar el fallo. Solo una creación exitosa proporciona un hilo cuya terminación se pueda esperar.

Espera a la terminación antes de leer los resultados

Después de crear correctamente ambos trabajadores, el hilo principal llama a pthread_join(t[0], NULL) y pthread_join(t[1], NULL). La espera a la terminación bloquea hasta que termina el hilo indicado. Una espera exitosa también hace que las escrituras de ese trabajador sean visibles para el hilo que esperó. El hilo principal puede entonces leer 1275 y 3775 y sumarlos para obtener 5050 sin un mutex: los trabajadores escribieron en objetos separados y el hilo principal esperó antes de leerlos.

Iniciar ambos hilos antes de esperar a su terminación permite que su trabajo se solape. Crear un trabajador y esperar inmediatamente a que termine antes de crear el siguiente hace que los trabajadores se ejecuten uno después del otro.

Este es el programa principal. Termina si hay un error de pthread, por lo que nunca usa un identificador de una creación fallida:

static void require_success(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    struct range jobs[2] = {{1, 50, 0}, {51, 100, 0}};
    pthread_t threads[2];
    for (int i = 0; i < 2; i++)
        require_success(pthread_create(&threads[i], NULL,
                                      sum_range, &jobs[i]));
    for (int i = 0; i < 2; i++)
        require_success(pthread_join(threads[i], NULL));
    printf("%ld\n", jobs[0].result + jobs[1].result);
    return 0;
}

Para recoger un puntero devuelto, pasa la dirección donde se recibirá: void *answer; pthread_join(t, &answer);. El puntero debe referirse a memoria que siga existiendo después de que termine el trabajador, como memoria asignada en el montículo o una estructura que pertenezca al creador. Devolver &local_result desde el trabajador no es válido porque su variable local deja de existir cuando la función retorna.

Evita compartir una variable de bucle que cambia

Pasar &i a cada trabajador en un bucle de creación pasa repetidamente una misma dirección. Los trabajadores pueden leerla después de que el bucle la haya cambiado, por lo que varios pueden recibir el mismo identificador aparente. Usa un arreglo en su lugar: asigna ids[i] = i, pasa &ids[i] y mantén el arreglo en memoria y sin cambios hasta que terminen todas las esperas.

Termina hilos y procesos de forma deliberada

Retornar de main o llamar a exit() en cualquier hilo termina todo el proceso y puede interrumpir a los trabajadores. El hilo principal normalmente espera a que los trabajadores terminen antes de retornar. Llamar a pthread_exit() solo termina el hilo que llama; los demás pueden continuar.

Un hilo separado libera automáticamente sus recursos de hilo cuando termina y no se puede esperar a su terminación. Asegúrate de esperar una vez a la terminación de cada hilo que lo permita o de separarlo. Esperar a la terminación no protege a dos trabajadores que actualizan el mismo objeto mientras ambos se ejecutan.

Puntos clave

  • Comprueba directamente los códigos de retorno de pthread; espera solo a la terminación de los hilos creados correctamente.
  • Pasa objetos de argumentos distintos y estables y mantenlos en memoria hasta que terminen los trabajadores.
  • Espera a la terminación de un trabajador antes de leer un resultado que todavía pueda estar escribiendo.
  • Devuelve solo punteros a objetos que sigan existiendo después de que termine el trabajador.
  • Separa un hilo cuando sus recursos deban recuperarse sin esperar a su terminación.
  • Retornar de main termina el proceso, incluidos los trabajadores que no hayan terminado.
2

Identifica los pasos compartidos que pueden interferir

Una sentencia del código fuente puede ocultar varias operaciones máquina. Seguir esas operaciones en orden hace visible un fallo de concurrencia: un hilo lee, otro cambia el objeto y el primero guarda una respuesta basada en su lectura antigua. Usa seguimientos para encontrar la secuencia compartida que debe permanecer unida. Después distingue una carrera de datos de un error más amplio de temporización y evalúa una solución según la exclusión, el progreso y la espera.

2.1 Sigue una actualización perdida instrucción por instrucción

La sentencia counter++ parece pequeña, pero el incremento ordinario de un entero compartido no es una operación indivisible. Suele cargar un valor, sumar en un registro y guardar el resultado. Otro hilo puede ejecutarse entre esos pasos.

Un seguimiento que pierde un incremento

Empieza con counter igual a 50. Trata la carga, la suma y el almacenamiento como pasos de máquina separados:

PasoHilo en ejecuciónAcciónContador compartido
1T1Carga 50 y suma 1 en su registro50
2T2Carga 50 y suma 1 en su registro50
3T2Guarda el valor de su registro, 5151
4T1Guarda el valor de su registro, 5151

Dos incrementos deberían haber producido 52. En cambio, T1 sobrescribe el resultado de T2 con un valor calculado a partir del 50 anterior. Esto es una actualización perdida.

Un cambio de contexto conserva los registros de T1 en su TCB. Cuando T1 reanuda su ejecución, su registro sigue conteniendo 51; no vuelve a cargar el contador compartido solo porque T2 se haya ejecutado. El error está en la secuencia sin protección, no en que el planificador restaure un registro incorrecto.

Compara con una ejecución en serie

Si T1 completa los tres pasos antes de que T2 cargue el valor, T1 guarda 51 y después T2 guarda 52. Invertir el orden de los hilos completos también da 52. El resultado intercalado 51 no puede surgir de ninguno de los dos órdenes en serie. Para encontrar este tipo de fallo, marca una lectura, coloca después de ella la escritura de otro hilo y reanuda el cálculo del primer hilo basado en el valor antiguo.

Este seguimiento es un modelo del error a nivel de máquina. En C real, los accesos conflictivos sin sincronizar a un entero compartido ordinario forman una carrera de datos, que produce un comportamiento indefinido. C no limita ese programa a los resultados de un modelo sencillo de intercalado de instrucciones.

Por qué superar las pruebas es una prueba débil

Dos hilos que realizan 10 000 000 de incrementos cada uno deberían producir 20 000 000. Las ejecuciones experimentales sin protección pueden imprimir distintos totales menores, como 19 345 221 y 19 221 041 en ejemplos publicados. Una ejecución con buen resultado solo significa que el resultado observado fue aceptable por casualidad.

El intervalo vulnerable puede tener solo unas pocas instrucciones. Distintos números de núcleos, opciones del compilador o cargas del sistema pueden revelar un orden de ejecución que miles de ejecuciones anteriores no detectaron. Para dos hilos que realizan cada uno un incremento modelado desde cero, el modelo de seguimiento permite 1 o 2. Con al menos dos incrementos por hilo, órdenes de ejecución poco frecuentes pueden dejar un total tan bajo como 2 incluso cuando aumenta el número de incrementos previstos.

Corrige la operación, no su apariencia

Declarar el entero como volatile no hace indivisibles su carga, suma y almacenamiento ni sincroniza los hilos. Escribir todo el trabajo en una línea no cambia nada. Incluso una instrucción x86 que suma directamente en memoria necesita la forma atómica adecuada para coordinarse entre núcleos.

Protege toda la lectura-modificación-escritura con un mutex compartido o usa una operación atómica de lectura-modificación-escritura de C11 como atomic_fetch_add. La misma necesidad existe en un núcleo: un cambio provocado por el temporizador puede separar la carga del almacenamiento incluso sin ejecución en paralelo.

Puntos clave

  • Un incremento ordinario puede consistir en una carga, una suma y un almacenamiento.
  • Un almacenamiento basado en un valor antiguo puede sobrescribir la actualización completada de otro hilo.
  • Los cambios de contexto conservan los registros privados de cada hilo.
  • Volatile no proporciona atomicidad ni sincronización entre hilos.
  • Una carrera de datos en C tiene comportamiento indefinido; un seguimiento ilustra el fallo sin limitar sus posibles resultados.

2.2 Mantén juntas una decisión y su actualización

Un error de concurrencia puede afectar a un acceso a memoria o a toda una regla de negocio. Identifica tanto los objetos compartidos como la operación que debe parecer indivisible a los demás hilos.

¿Condición de carrera o carrera de datos?

Una condición de carrera se produce cuando la corrección depende de los tiempos relativos. Una carrera de datos se produce cuando distintos hilos acceden a la misma ubicación de memoria sin sincronización, al menos uno escribe en ella y al menos un acceso no es atómico. C11 y C++11 asignan comportamiento indefinido a una carrera de datos.

Los accesos atómicos pueden evitar una carrera de datos mientras una operación más amplia sigue teniendo una condición de carrera. Por ejemplo, las lecturas y escrituras protegidas individualmente no mantienen válida una decisión entre unas y otras. Dos hilos pueden leer cada uno un saldo suficiente, liberar el cerrojo y retirar después dinero basándose en esa decisión desactualizada.

Analiza una retirada

Un saldo empieza en 100. Dos hilos solicitan 80 cada uno. La regla es aceptar una retirada solo cuando el saldo la cubre.

  1. T1 comprueba 100 y decide que su retirada puede tener éxito.
  2. T2 también comprueba 100 y toma la misma decisión.
  3. T1 resta 80 y deja 20.
  4. T2 actúa según su decisión anterior y resta 80 de 20, dejando −60.

Proteger solo la resta no corrige esa secuencia. Protege la comprobación y la resta con el mismo cerrojo, mantenido de forma continua. Entonces un hilo deja 20 y el siguiente comprueba 20 y rechaza la retirada. Otro orden de ejecución sin protección puede perder una resta y dejar 20 después de haber pagado ambas retiradas, lo cual también es incorrecto.

Encuentra la sección crítica

Una sección crítica es la región que accede a datos compartidos y debe excluir los accesos de otros hilos. Una secuencia de lectura-modificación-escritura primero lee datos, calcula un valor nuevo y lo vuelve a escribir. Una secuencia de comprobar y actuar decide si se permite una acción y después la realiza. Las operaciones con listas, la inicialización diferida y la creación de archivos también pueden contener estos patrones.

Un cerrojo proporciona exclusión mutua: como máximo un hilo participante está dentro de la región protegida a la vez. Una operación atómica aparece ante los demás hilos participantes como una sola operación completa; no pueden observar un estado a medio terminar. Un mutex proporciona esa visión solo para el código que sigue el mismo acuerdo de uso del cerrojo.

No todos los objetos necesitan un cerrojo. Los datos a los que accede un hilo permanecen privados salvo que se comparta su dirección. Los datos inicializados por completo antes de que comiencen los trabajadores y que después solo se leen pueden compartirse sin cerrojo. Evalúa quién puede acceder a un objeto y si las escrituras se solapan con su uso, no solo dónde se declaró.

Qué garantiza una solución correcta
  • Exclusión mutua: los hilos que compiten no pueden entrar juntos.
  • Progreso: cuando la sección está libre y hay hilos que quieren entrar, los hilos que no intentan entrar no pueden posponer indefinidamente la elección de uno que entre.
  • Espera acotada: después de una solicitud, hay un límite al número de entradas de otros hilos que pueden precederla.

atomic_fetch_add(&count, 1) de C11 realiza una actualización atómica sobre un atomic_int. La atomicidad está garantizada incluso en una plataforma que implementa la operación con un cerrojo interno de la biblioteca. Hacer atómicos por separado dos campos de cuentas sigue sin convertir una transferencia entre ellos en una transacción atómica; la relación entre los campos también necesita protección.

Puntos clave

  • Las carreras de datos afectan a accesos conflictivos a memoria sin sincronizar; las condiciones de carrera afectan a la corrección que depende de los tiempos.
  • Una comprobación y la acción que depende de ella pertenecen a una misma sección crítica.
  • La exclusión mutua, el progreso y la espera acotada son requisitos distintos.
  • Los accesos atómicos separados no hacen atómica una operación de varios pasos.
3

Protege las operaciones compartidas con mutex

Un cerrojo funciona mediante un acuerdo compartido: toda operación sobre los datos protegidos sigue la misma regla. Aprende el ciclo de vida de un mutex POSIX y después decide dónde empieza y termina una sección crítica. Las secciones cortas reducen la espera, pero una comprobación debe permanecer junto a la acción que autoriza. Revisar el código en busca de liberaciones omitidas, cerrojos que no coinciden y tiempos de vida inseguros convierte ese acuerdo en algo que puedes revisar y mantener.

3.1 Usa un mutex desde su inicialización hasta su liberación

Un mutex es un cerrojo que permite que un hilo a la vez posea una región protegida. Un hilo lo adquiere antes de acceder a los datos protegidos y lo libera después. Si otro hilo lo posee, una llamada bloqueante para adquirirlo espera.

Inicializa el mutex compartido

Usa pthread_mutex_t de <pthread.h>. Un mutex estático puede empezar con pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;. Un objeto creado dinámicamente puede inicializar su campo con pthread_mutex_init(&obj->lock, NULL). La inicialización debe terminar antes de que cualquier hilo use el mutex.

Este es un ejemplo de operación sobre un contador que usa un mutex compartido inicializado de forma estática. La función auxiliar comprueba los valores devueltos por pthread, que son directamente números de error:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static long counter = 0;  // guarded by m

static void check(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

void increment(void) {
    check(pthread_mutex_lock(&m));
    counter = counter + 1;
    check(pthread_mutex_unlock(&m));
}

Cada lectura o actualización concurrente de counter debe seguir esta misma regla. Una función independiente que use otro mutex no se coordinaría con increment.

¿Qué ocurre mientras se posee el mutex?

T1 adquiere m, carga el contador, calcula y guarda el resultado. Si T2 llama a pthread_mutex_lock(&m) durante ese intervalo, no puede superar la llamada hasta adquirir el mutex después de que T1 lo libere. A T1 aún se le puede expropiar la CPU: un mutex impide la entrada de otros hilos, no los cambios de contexto.

Para la regla del saldo, adquiere el mutex antes de evaluar balance >= amount. Resta solo si se cumple, registra el éxito o el fallo y después libera el cerrojo. Un saldo de 100 con dos solicitudes de 80 permite exactamente un éxito y deja 20.

Elige cómo esperar

pthread_mutex_trylock(&m) retorna inmediatamente: 0 significa que quien llamó adquirió el mutex; EBUSY significa que no lo hizo porque el mutex estaba ocupado. La ruta en la que está ocupado puede realizar otro trabajo y no debe acceder a los datos protegidos. pthread_mutex_timedlock espera hasta un plazo absoluto. Estas interfaces siguen requiriendo comprobar los códigos de retorno y liberar un cerrojo solo después de adquirirlo correctamente.

Respeta la propiedad y el tiempo de vida

El hilo propietario libera un mutex. Un mutex normal no recursivo bloquea si su propietario intenta adquirirlo de nuevo. Un mutex con comprobación de errores informa de fallos como volver a adquirirlo mediante EDEADLK o que un hilo no propietario lo libere mediante EPERM. Un mutex recursivo cuenta las adquisiciones repetidas de su propietario y requiere el mismo número de liberaciones. No dependas de un mutex predeterminado para detectar usos incorrectos: su comportamiento depende del tipo al que lo asigna la implementación y de la operación.

Cuando todos los hilos hayan dejado de usar el mutex y esté desbloqueado, llama a pthread_mutex_destroy. Destruir o liberar un objeto mientras otro hilo todavía puede adquirir su mutex rompe el tiempo de vida del mutex. Pasar el mutex mediante un puntero conserva su identidad; copiarlo no crea otro identificador válido para el mismo cerrojo.

Puntos clave

  • Inicializa un mutex antes de ponerlo a disposición de los trabajadores.
  • Adquiere el mismo mutex compartido antes de cada acceso concurrente a los datos que protege.
  • Un mutex adquirido impide la entrada de otros hilos, aunque a su propietario se le pueda expropiar la CPU.
  • Si el intento de adquisición sin espera devuelve EBUSY, significa que no se adquirió la propiedad.
  • Mantén el mutex en memoria hasta que ningún hilo pueda volver a usarlo.
  • Los mutex con comprobación de errores informan de errores de propiedad; los mutex recursivos cuentan las adquisiciones anidadas.

3.2 Elige qué protege cada cerrojo

La granularidad de los cerrojos es la cantidad de estado compartido que cubre un cerrojo. Determina qué operaciones deben esperarse unas a otras, incluso cuando se ejecutan en núcleos distintos.

¿Un cerrojo grande o varios más pequeños?

Un diseño de grano grueso coloca un mutex alrededor de toda una estructura o subsistema. Tiene una regla de protección sencilla, pero las operaciones sin relación entre sí se serializan. Un diseño de grano fino asigna mutex distintos a distintas cuentas, cubetas o regiones. Así, los hilos pueden trabajar en distintas regiones a la vez, a costa de más operaciones con cerrojos y reglas más complicadas.

Coloca el cerrojo junto al estado que protege:

struct account {
    pthread_mutex_t lock;
    long balance;
    unsigned id;
};

Una operación sobre una cuenta adquiere su mutex. Una transferencia entre dos cuentas distintas necesita ambos porque cambia la relación entre ellas. Elige un orden fijo, como el orden creciente de identificador de cuenta, y úsalo en todas las rutas. De lo contrario, un hilo puede poseer A y esperar B mientras otro posee B y espera A.

Un cerrojo pone un límite a la tasa de procesamiento

Supón que cada operación posee un único cerrojo global durante 2 µs. El trabajo protegido puede completar como máximo una operación cada 2 µs:

tasa maˊxima de procesamiento=12×10−6=500 000 operaciones por segundo.

Añadir núcleos no puede hacer que dos operaciones posean el mismo mutex simultáneamente. La sobrecarga del cerrojo y otro trabajo pueden reducir aún más la tasa alcanzada. Por tanto, la duración de la parte protegida es un límite útil que calcular antes de esperar una aceleración.

Acorta la sección sin romper la operación

Prepara el trabajo privado antes de adquirir el mutex. Para insertar en una lista, asigna primero el nuevo nodo e inicializa su clave. Adquiere el mutex de la lista solo al leer el nodo inicial actual y enlazar el nodo. Si falla la asignación de memoria, no habrá ningún cerrojo adquirido.

Las operaciones lentas de entrada/salida, la impresión o una función de retorno desconocida pueden mantener detenidos a todos los hilos en espera. Traslada ese trabajo fuera cuando los datos puedan seguir siendo válidos allí. Mantén juntas una decisión y su actualización dependiente: acortar una retirada liberando el cerrojo entre la comprobación del saldo y la resta destruye la corrección.

Evalúa tres propiedades distintas

La exclusión mutua plantea si el diseño impide realmente la entrada de hilos que compiten. La equidad plantea si un hilo en espera acaba obteniendo un turno o puede sufrir inanición. El rendimiento plantea cuánto cuesta el cerrojo con un hilo, con varios hilos en una CPU y con hilos en varias CPU. Un cerrojo puede ser correcto y aun así ser injusto o lento.

La contención ocurre cuando los hilos compiten por un cerrojo ya ocupado. Con mucha contención, también trasladan líneas de caché compartidas del cerrojo y de los datos entre núcleos. Un experimento publicado con un contador tardó unos 0,03 s para un millón de actualizaciones de un hilo, pero más de 5 s para dos hilos que hacían un millón cada uno; son mediciones de esa configuración, no tiempos universales.

Para un experimento de escalabilidad, mantén fijo el trabajo de cada hilo. La escalabilidad ideal permite que N hilos en N núcleos hagan N veces el trabajo en el tiempo original. Empieza con una estructura sencilla y correcta, mide la espera y el tiempo total, y divide un cerrojo solo donde la concurrencia adicional compense su sobrecarga.

Puntos clave

  • Los cerrojos de grano grueso simplifican la protección; los de grano más fino permiten que regiones independientes avancen a la vez.
  • Un solo cerrojo serializa el trabajo que protege y limita la tasa de procesamiento.
  • Adquiere varios cerrojos siempre en un orden fijo.
  • Evalúa por separado la exclusión, la equidad y el rendimiento.
  • Mide antes de sustituir un cerrojo sencillo por un diseño más complicado.

3.3 Revisa todas las rutas que usan el cerrojo

Un mutex puede estar implementado perfectamente y aun así un programa puede usarlo de forma incorrecta. Revisa toda la ruta de una función, incluidas las lecturas, los errores, las funciones de retorno y la destrucción de objetos.

Libera la propiedad en cada salida

Esta función deja el cerrojo adquirido en su ruta de fallo:

pthread_mutex_lock(&m);
if (failed) return -1;
update_shared_state();
pthread_mutex_unlock(&m);

El hilo que retorna puede terminar, pero el mutex sigue ocupado. Quien llame después esperará una liberación que nunca ocurrirá. Usa una sola ruta de liberación de recursos o coloca una liberación del cerrojo antes de cada retorno. Con llamadas a pthread comprobadas, la estructura puede ser:

int rc = -1;
check(pthread_mutex_lock(&m));
if (!failed) {
    update_shared_state();
    rc = 0;
}
check(pthread_mutex_unlock(&m));
return rc;

Es más fácil revisar un punto de liberación que muchos dispersos. La propiedad importante es que cada cerrojo adquirido correctamente se libere en cada salida ordinaria.

Sigue la regla de protección también en las lecturas

Una función de estado que «solo lee» puede tener una carrera con otra que actualiza. También puede observar campos relacionados en distintas etapas de una actualización. Si una operación de lista publica un nodo antes de completar sus campos, un lector sin protección puede seguir un puntero hacia datos sin terminar. Adquiere el cerrojo documentado para cada acceso cuya corrección dependa de él.

Usar mutex distintos para el mismo objeto no ayuda. Si T1 adquiere lockA y T2 adquiere lockB, ambas adquisiciones pueden tener éxito. Un mutex declarado como una variable local nueva dentro de cada trabajador hace que este error sea fácil de pasar por alto: cada hilo adquiere su propio objeto, por lo que no hay exclusión compartida.

Conserva tanto la identidad como los límites de la operación

No se puede copiar un pthread_mutex_t mediante una asignación o copiando la estructura que lo contiene y después usarlo como un segundo identificador de mutex. Usar la copia es un comportamiento indefinido. Pasa un puntero al mutex compartido original.

Comprueba también dónde se producen las liberaciones. Una función que adquiere el cerrojo para comprobar un saldo, lo libera y después vuelve a adquirirlo para restar ha protegido los accesos individuales, pero ha dejado que la decisión quede desactualizada. Mantén toda la secuencia de comprobar y actuar bajo una posesión continua del cerrojo.

Evita esperarte a ti mismo

Llamar a código desconocido mientras se posee un mutex puede volver a entrar inesperadamente en el mismo objeto protegido. Con un mutex normal, una segunda llamada de adquisición por parte del propietario lo bloquea. Un mutex con comprobación de errores informa de este fallo, mientras que un mutex recursivo permite la propiedad anidada y necesita las liberaciones correspondientes. Elige un tipo de forma deliberada; no uses la recursividad para ocultar un diseño de protección poco claro.

Mantén las operaciones lentas de entrada/salida y las suspensiones fuera de una sección crítica compartida cuando sea posible. Antes de destruir el mutex o liberar el objeto que lo contiene, asegúrate de que ningún hilo lo posee ni puede volver a acceder a él. La inicialización, el uso y la destrucción deben seguir un orden claro.

Usa herramientas de ejecución para revelar fallos observados

ThreadSanitizer, activado con -fsanitize=thread, y Helgrind de Valgrind pueden informar de carreras de datos y usos incorrectos de cerrojos en las ejecuciones que observan. Combina sus informes con una revisión de los accesos compartidos y de los posibles intercalados. Una ejecución sin informes establece que la herramienta no encontró problemas en esas ejecuciones; una ruta no probada sigue requiriendo razonamiento.

Puntos clave

  • Libera cada mutex adquirido correctamente en todas las rutas de salida.
  • Los lectores deben seguir la regla de protección cuando los escritores pueden ejecutarse de forma concurrente.
  • Un mutex se comparte mediante su dirección; los mutex separados o copiados no coordinan el acceso.
  • No separes una comprobación de su acción liberando el cerrojo entre ellas.
  • Las funciones de retorno desconocidas pueden volver a entrar en el código e intentar adquirir el mutex ocupado.
  • Una ejecución sin informes del detector de carreras cubre las ejecuciones observadas, así que revisa también las rutas no probadas.
4

Cómo se adquieren, esperan y transfieren los cerrojos

Un cerrojo debe hacer indivisible la decisión sobre su propiedad y ordenar los accesos protegidos a memoria. Los indicadores ordinarios no hacen ninguna de las dos cosas. Parte de intentos fallidos de implementar cerrojos para llegar al intercambio atómico y después añade equidad con números de boleto. Por último, considera al hilo en espera: la espera activa, la cesión de la CPU y la suspensión tienen costes distintos, y la suspensión necesita un protocolo que no pierda un despertar. La prioridad de planificación puede cambiar qué estrategia de espera logra avanzar.

4.1 Por qué no basta con comprobar un indicador ordinario

Un cerrojo necesita una forma fiable de elegir un único propietario. Una lectura ordinaria seguida de una escritura ordinaria deja un hueco en esa decisión, aunque el propio trabajo protegido sea muy corto.

El control de interrupciones tiene una función limitada

En una CPU, un núcleo puede desactivar brevemente las interrupciones alrededor de una sección crítica no bloqueante para impedir que una interrupción provoque una expropiación dentro de ella. Esto depende de permanecer en la CPU sin bloquearse ni cambiar de hilo voluntariamente. El núcleo usa la técnica para operaciones internas breves.

No es un mutex de aplicación de uso general. Las instrucciones de control de interrupciones son privilegiadas; permitir que una aplicación las use sin restricciones le permitiría monopolizar la CPU. Desactivar las interrupciones en un núcleo no impide que los demás accedan a los mismos datos. Los intervalos largos con interrupciones desactivadas también retrasan su atención y pueden provocar la pérdida de eventos en algunos dispositivos.

Sigue el cerrojo defectuoso basado en un indicador

Considera este pseudocódigo similar a C:

while (flag == 1) { }
flag = 1;
// critical section
flag = 0;

Empieza con flag en 0. T1 lo comprueba y sale del bucle; después se detiene antes de la asignación. T2 comprueba el mismo 0, lo pone en 1 y entra. T1 reanuda su ejecución, lo pone en 1 y también entra. Ambos superaron la comprobación antes de que cualquiera reclamara la propiedad.

Hacer atómicas por separado la lectura y la escritura eliminaría una carrera de datos sobre el indicador, pero seguiría dejando dos operaciones separadas en este protocolo. La adquisición de la propiedad necesita una lectura-modificación-escritura atómica o un algoritmo de sincronización más completo.

El bucle vacío es espera activa: una comprobación repetida que consume tiempo de CPU. En una CPU, un hilo que realiza espera activa después de que se haya expropiado la CPU al propietario no puede hacer que este la libere antes. Debe acabar permitiendo que el propietario se ejecute.

La solución de Peterson para dos hilos

El algoritmo de Peterson coordina dos hilos con un indicador de interés por hilo y un turno compartido. El hilo i realiza:

flag[i] = 1;
turn = 1 - i;
while (flag[1 - i] == 1 && turn == 1 - i) { }
// critical section
flag[i] = 0;

Anuncia su interés, da prioridad al otro hilo en caso de empate y espera solo mientras el otro esté interesado y tenga prioridad. Bajo consistencia secuencial, las operaciones de cada hilo conservan el orden del programa y todos los hilos observan un orden común. En ese modelo, el algoritmo de Peterson proporciona exclusión mutua, progreso y espera acotada para dos hilos.

El orden de memoria también importa

Los procesadores y compiladores modernos pueden hacer que las operaciones de memoria sean visibles en un orden distinto del código fuente. Un almacenamiento puede permanecer en un búfer mientras una carga posterior lee la memoria. Entonces ambos hilos pueden ver el indicador de interés del otro como 0 y entrar ambos. Los enteros compartidos ordinarios de C también introducen carreras de datos de C.

Una implementación válida necesita operaciones atómicas y una ordenación adecuadas, como operaciones atómicas de C11 con consistencia secuencial para los indicadores y el turno. volatile no proporciona esa sincronización. Los cerrojos prácticos combinan una operación indivisible de adquisición de propiedad con garantías de ordenación de memoria para que los accesos protegidos permanezcan en el lado correcto de la adquisición y la liberación.

Puntos clave

  • Separar la comprobación de la asignación permite que dos hilos reclamen el mismo cerrojo libre.
  • Desactivar las interrupciones en una CPU no excluye a otro núcleo.
  • El algoritmo de Peterson para dos hilos requiere un comportamiento con consistencia secuencial.
  • Tanto la adquisición atómica de la propiedad como una ordenación correcta de memoria son necesarias para un cerrojo práctico.

4.2 Usa una instrucción atómica para elegir un propietario

Una lectura-modificación-escritura atómica combina la lectura de una ubicación y su actualización en una acción indivisible. Elimina el hueco que permite que dos hilos superen juntos la comprobación de un indicador ordinario.

Intercambia el indicador y examina su valor anterior

Comprobar y establecer, o intercambio atómico, escribe un valor nuevo y devuelve el anterior. Con un indicador inicialmente en 0, T1 intercambia el valor por 1 y recibe 0, por lo que entra. T2 intercambia el valor por 1 y recibe 1, por lo que espera. Hasta que la liberación escriba 0, ningún otro intercambio puede devolver 0.

Este es un cerrojo de espera activa de C11 que usa las operaciones atómicas predeterminadas con consistencia secuencial:

#include <stdatomic.h>

static atomic_flag held = ATOMIC_FLAG_INIT;

void lock(void) {
    while (atomic_flag_test_and_set(&held)) { }
}

void unlock(void) {
    atomic_flag_clear(&held);
}

La ordenación importa tanto como el indicador. La adquisición debe mantener los accesos posteriores de la sección crítica después de la adquisición exitosa; la liberación debe mantener los accesos anteriores de la sección crítica antes de la liberación. Borrar el indicador también debe ser atómico; mezclar almacenamientos ordinarios con accesos atómicos no implementa esta transferencia.

Compara antes de sustituir

Comparar e intercambiar, o CAS, cambia un valor solo cuando es igual a un valor esperado. Supón que dos hilos leen un contador con valor 7 y cada uno quiere sumar 1. T2 cambia correctamente 7 por 8. El intento de T1 de cambiar 7 por 8 falla ahora, por lo que T1 vuelve a intentarlo desde 8 y lo cambia por 9. El reintento conserva ambos incrementos.

atomic_compare_exchange_strong de C11 devuelve éxito o fallo y actualiza el argumento esperado en caso de fallo. En cambio, algún pseudocódigo de estilo hardware devuelve el valor anterior almacenado. Comprueba qué convención usa un seguimiento. En x86, xchg con memoria implementa el intercambio, mientras que lock cmpxchg proporciona comparar e intercambiar de forma atómica.

Otras piezas básicas de hardware

Carga enlazada y almacenamiento condicional, o LL/SC, lee mientras establece una reserva y después intenta escribir. La escritura condicional falla si otra escritura invalida la reserva; también puede fallar por otros motivos, por lo que pertenece a un bucle de reintentos. LDXR/STXR de ARM y lr/sc de RISC-V admiten este patrón.

Obtener y sumar suma atómicamente y devuelve el valor anterior. C11 lo ofrece como atomic_fetch_add; x86 proporciona lock xadd. Devolver el valor anterior lo hace útil para repartir números de boleto únicos.

C garantiza el comportamiento atómico de las operaciones de su biblioteca atómica. No exige que todos los tipos atómicos sean sin cerrojos; algunas operaciones usan un cerrojo interno de la biblioteca. atomic_flag tiene la garantía de funcionar sin cerrojos, y atomic_is_lock_free comprueba otros objetos.

Correcto no significa equitativo ni rápido

Un cerrojo con comprobar y establecer proporciona exclusión mutua, pero no orden de llegada: un hilo puede perder repetidamente la carrera después de la liberación y sufrir inanición. En una CPU, cuatro hilos con intervalos de 10 ms de planificación por turnos pueden desperdiciar 30 ms en espera activa entre tres hilos en espera antes de que el propietario al que se expropió la CPU vuelva a ejecutarse. En varios núcleos, con el propietario en ejecución y secciones cortas, una breve espera activa puede evitar la sobrecarga de suspender y despertar.

Puntos clave

  • Comprobar y establecer devuelve el indicador anterior mientras escribe atómicamente el nuevo.
  • CAS vuelve a intentarlo tras encontrar un valor esperado desactualizado, conservando las actualizaciones concurrentes.
  • El código LL/SC debe gestionar el fallo del almacenamiento condicional volviendo a intentarlo.
  • Obtener y sumar devuelve el valor anterior al realizar el incremento.
  • La adquisición y la liberación de un cerrojo también deben ordenar los accesos protegidos a memoria.
  • Un cerrojo de espera activa con comprobar y establecer proporciona exclusión, pero no garantiza que se evite la inanición.

4.3 Reparte turnos con un cerrojo por boletos

Un cerrojo con comprobar y establecer recuerda si está ocupado, pero no quién ha estado esperando. Un cerrojo por boletos añade un orden de llegada mediante un dispensador atómico de boletos y un contador de turno.

Toma un número y espera tu turno

Ambos contadores empiezan en 0. Cada hilo que llama incrementa atómicamente ticket y conserva su valor anterior como mine. Solo entra cuando turn es igual a mine. El propietario libera el cerrojo incrementando turn una vez.

#include <stdatomic.h>

static atomic_uint ticket = 0;
static atomic_uint turn = 0;

void lock(void) {
    unsigned mine = atomic_fetch_add(&ticket, 1);
    while (atomic_load(&turn) != mine) { }
}

void unlock(void) {
    atomic_fetch_add(&turn, 1);
}

Estas operaciones atómicas predeterminadas de C11 proporcionan tanto atomicidad como ordenación con consistencia secuencial. Cada hilo que llama conserva mine en su propia variable local. El contador compartido de boletos dispensa números únicos para los boletos pendientes.

Sigue tres llegadas

A toma el boleto 0 y entra porque turn es 0. B toma el boleto 1 y espera. C toma el boleto 2 y espera. El siguiente boleto sin usar es ahora el 3, mientras que el turno actual sigue siendo 0.

Cuando A libera el cerrojo, turn pasa a 1, por lo que B entra. C sigue esperando el 2. La liberación de B hace avanzar turn a 2 y C entra. Un hilo que llega después obtiene el boleto 3 y no puede adelantarse a C.

Esta distinción importa al seguir el funcionamiento: ticket registra el siguiente número que se entregará; turn registra el número que se está atendiendo. Tomar un boleto solo cambia el dispensador. La liberación hace avanzar la atención.

Qué garantiza la equidad

Un hilo espera detrás de los hilos con boletos anteriores. Si cada propietario acaba liberando el cerrojo y los hilos que tiene delante pueden ejecutarse, las llegadas posteriores no pueden adelantarlo. El número de secciones críticas de otros hilos que tiene delante está limitado por los boletos anteriores pendientes. Esto proporciona espera acotada y evita la inanición bajo esas condiciones de progreso.

La garantía se refiere al orden de entrada, no al tiempo transcurrido. Un propietario que se detiene durante mucho tiempo puede hacer que todos los que llegan después también esperen mucho tiempo. Un número de boleto no puede obligar al planificador a ejecutar a su propietario.

Costes que permanecen con el orden por boletos

Cada hilo en espera sigue realizando espera activa. Si el hilo al que le toca entrar a continuación deja de ejecutarse, los hilos posteriores que están listos esperan aunque ningún hilo ocupe actualmente la sección crítica. Este es un coste de planificación del orden estricto.

Los hilos en espera también leen una única ubicación compartida turn. Cada liberación cambia la línea de caché que todos observan, lo que produce tráfico entre núcleos. Los cerrojos de espera activa basados en colas reducen ese tráfico al permitir que cada hilo observe una ubicación local separada; Linux pasó de cerrojos de espera activa por boletos a diseños basados en colas.

Los contadores de boletos reales usan aritmética sin signo. Las comprobaciones de igualdad pueden funcionar tras el retorno cíclico cuando el número de boletos pendientes se mantiene por debajo del rango de valores del contador. Reutilizar un número mientras un boleto anterior igual aún está pendiente destruiría la unicidad y la exclusión.

Puntos clave

  • La asignación de boletos es atómica; quien llama solo entra cuando su número coincide con el turno actual.
  • La liberación hace avanzar turn, mientras que la adquisición asigna un número de ticket.
  • Los propietarios de boletos anteriores no pueden ser adelantados por quienes llegan después.
  • El orden equitativo sigue dejando espera activa, detenciones por planificación y tráfico de caché compartida.

4.4 Permite que los hilos se suspendan sin perder una liberación

Un cerrojo correcto puede desperdiciar la mayor parte de una CPU en esperas. La estrategia de espera adecuada depende de si el propietario puede ejecutarse y de cuánto tiempo poseerá el cerrojo.

Ceder la CPU abandona un turno

Un hilo en espera puede llamar a yield() después de un intento fallido de adquirir la propiedad. La cesión lo pasa del estado de ejecución al estado listo para que el planificador pueda seleccionar otro hilo. Sigue estando listo para ejecutarse, por lo que puede ser seleccionado de nuevo pronto y volver a fallar. Con 100 hilos en una CPU, hasta 99 hilos en espera pueden ejecutarse y ceder la CPU antes de que el propietario al que se expropió la CPU reciba otro turno. La cesión reduce la espera activa, pero no proporciona un orden de entrada equitativo.

El bloqueo retira al hilo del trabajo listo para ejecutarse

Un cerrojo que suspende hilos coloca en una cola de espera al hilo que no consigue adquirirlo. No consume CPU mientras está bloqueado. Al liberar el cerrojo se despierta a un hilo en espera para que pueda volver a ejecutarse. Un pequeño cerrojo protector interno de espera activa puede proteger el indicador y la cola; ese protector solo se posee durante breves operaciones de gestión.

Separar «decidir suspenderse» de «suspenderse realmente» crea un riesgo de despertar perdido:

  1. Un hilo en espera se coloca en la cola y libera el protector.
  2. Se detiene antes de llamar a park().
  3. El propietario libera el cerrojo y llama a unpark() para ese hilo.
  4. Si se descarta ese despertar, el hilo se suspende después sin que quede nadie para despertarlo.

setpark() de Solaris anuncia la intención de suspenderse. Un unpark que intervenga antes hace que el park posterior retorne inmediatamente. El protocolo conecta la decisión con el bloqueo real.

Linux usa una comparación y bloqueo atómicos

Un futex combina una palabra en memoria de usuario con soporte del núcleo para la espera. futex_wait(addr, expected) bloquea solo si la palabra sigue siendo igual al valor esperado; la comprobación y la decisión de bloquear son atómicas con respecto a las operaciones sobre esa palabra de futex. Si una liberación ya la cambió, el hilo que llama no se suspende basándose en la expectativa desactualizada.

futex_wake(addr, n) despierta hasta n hilos en espera. Un cerrojo sin contención puede adquirirse y liberarse con instrucciones atómicas en el espacio de usuario, evitando una llamada al sistema. Los mutex de pthread de glibc de Linux usan futexes para las esperas con contención. Un despertar deja a un hilo listo para ejecutarse; su protocolo de cerrojo aún debe establecer la propiedad antes de acceder a los datos protegidos.

Realiza una breve espera activa y después suspéndete

Un cerrojo de dos fases primero realiza una breve espera activa y después se bloquea si la espera continúa. La espera activa puede merecer la pena cuando el propietario se ejecuta en otro núcleo y la espera prevista es más corta que suspender y despertar. El bloqueo es preferible para esperas largas o cuando hay más hilos listos para ejecutarse que núcleos. En una CPU, un hilo en espera activa no puede ayudar a terminar a un propietario que ha dejado de ejecutarse.

La prioridad de planificación puede detener el progreso

En la inversión de prioridades, un hilo de alta prioridad espera un mutex que posee un hilo de baja prioridad mientras el trabajo de prioridad media impide que el propietario use la CPU. Suspender al hilo de alta prioridad evita la espera activa, pero no elimina ese retraso de planificación.

La herencia de prioridades eleva temporalmente la prioridad del propietario a la del hilo en espera de mayor prioridad, permitiéndole terminar y liberar el cerrojo. El Mars Pathfinder de la NASA sufrió reinicios por inversión de prioridades en 1997; activar la herencia de prioridades del mutex corrigió el problema. Un hilo de alta prioridad en espera activa en una única CPU con planificación estricta por prioridades puede ser peor: puede impedir por completo que el propietario de baja prioridad se ejecute.

Puntos clave

  • Ceder la CPU mantiene listo al hilo en espera; suspenderlo lo bloquea y no consume CPU.
  • Un protocolo correcto de suspensión elimina el hueco entre decidir suspenderse y bloquearse realmente.
  • La espera de futex comprueba su valor esperado atómicamente con la decisión de bloquear.
  • Un cerrojo de dos fases realiza una breve espera activa antes de suspenderse.
  • La herencia de prioridades ayuda a un propietario de baja prioridad a liberar un cerrojo que necesita trabajo de mayor prioridad.
5

Construye estructuras compartidas que sigan siendo correctas

Una estructura es segura entre hilos cuando las llamadas concurrentes cumplen su especificación en todos sus posibles intercalados. Empieza con un mutex y una regla explícita para cada campo compartido. Después examina dónde las operaciones necesitan excluirse entre sí: las actualizaciones de contadores, los recorridos de listas, los extremos opuestos de una cola y las distintas cubetas de una tabla hash tienen necesidades diferentes. Cada mejora debe preservar la publicación segura y el tiempo de vida de la memoria, y justificar su complejidad adicional mediante mediciones.

5.1 Intercambia precisión del contador por menos actualizaciones compartidas

Un contador compartido es fácil de proteger con un mutex. Sus operaciones increment, decrement y get adquieren el mismo cerrojo, actúan y lo liberan. Es exacto, pero todas las actualizaciones pasan por un único punto compartido.

Por qué el contador exacto se convierte en un cuello de botella

En varios núcleos, un mutex serializa las operaciones del contador. Los núcleos también transfieren la propiedad de las líneas de caché del cerrojo y del contador. Añadir trabajadores puede aumentar la espera más rápido de lo que aumenta el trabajo útil. Sustituir el mutex por atomic_fetch_add elimina la sobrecarga explícita del cerrojo en procesadores habituales, pero cada actualización sigue escribiendo en la misma ubicación compartida.

Un contador aproximado distribuye las actualizaciones entre contadores locales y las transfiere a un contador global por lotes. El valor global que comunica omite deliberadamente los incrementos locales recientes.

Construye la actualización de local a global

Mantén un contador y un mutex por núcleo, un contador global con su propio mutex y un umbral S. Para cada incremento:

  1. Elige la posición local para esta actualización y adquiere su mutex.
  2. Suma 1 al recuento local.
  3. Si alcanza S, adquiere el mutex global mientras aún posees el local.
  4. Suma ese recuento local al global, restablece el recuento local a 0 y libera el mutex global.
  5. Libera el mutex local.

Mantén estable la posición elegida durante toda la actualización. Los hilos que usan distintas posiciones suelen acceder a distintos cerrojos y solo adquieren el mutex global durante una transferencia. La lectura del recuento global también sigue su regla de protección.

Sigue una transferencia por lotes

Con cuatro núcleos y S igual a 5, supón que sus totales de actualizaciones son 7, 3, 4 y 0. El núcleo 1 transfiere 5 en su quinto incremento y deja 2 incrementos locales después. Los recuentos locales finales son 2, 3, 4 y 0. El recuento global es 5 y el total real es 14: nueve incrementos permanecen en los contadores locales.

Después de completar actualizaciones de un incremento cada una, cada recuento local está entre 0 y S − 1, ambos incluidos. Con P núcleos, el total global puede quedar retrasado como máximo en:

P(S−1).

Para cuatro núcleos y S igual a 5, el límite es 16. Este límite se refiere a actualizaciones completadas y a sus recuentos locales restantes; no lo apliques a mitad de una transferencia.

Elige el umbral para el trabajo

Un umbral menor mejora la actualidad del valor, pero requiere más adquisiciones del cerrojo global. Con S igual a 1, cada incremento se transfiere globalmente, añadiendo la sobrecarga del cerrojo local sin evitar el cuello de botella global. Un umbral mayor mejora la escalabilidad de las actualizaciones, pero aumenta el posible retraso. En un experimento publicado, S igual a 1024 permitió que cuatro hilos realizaran cuatro millones de actualizaciones en casi el mismo tiempo que un hilo necesitó para un millón.

Una instantánea exacta debe poseer todos los mutex locales en un orden fijo y después el mutex global antes de sumar los recuentos. Ese orden concuerda con el de las actualizaciones, que adquieren un cerrojo local antes del global. La instantánea detiene las transferencias concurrentes mientras lee; leer cada campo por separado podría contar dos veces una transferencia o no contarla.

Los contadores aproximados sirven para estadísticas que toleran una visibilidad diferida. Un contador de referencias que determina cuándo puede liberarse la memoria necesita un método exacto: un recuento inferior al real podría liberar un objeto que aún está en uso.

Puntos clave

  • Un único contador atómico todavía puede ser un cuello de botella de caché compartida.
  • Los contadores aproximados agrupan los incrementos locales en un contador global protegido.
  • Después de completar actualizaciones de una unidad, el retraso máximo es P(S − 1).
  • Los umbrales de transferencia mayores reducen la contención global y aumentan el posible retraso.
  • Una instantánea exacta posee todos los cerrojos pertinentes en un orden compatible con las actualizaciones.

5.2 Publica y recorre nodos de listas enlazadas de forma segura

Una lista enlazada compartida necesita preservar tanto sus enlaces como el tiempo de vida de sus nodos. Un mutex alrededor de cada operación es un primer diseño útil porque todos los recorridos y modificaciones compartidos siguen una sola regla.

Cómo dos inserciones pierden un nodo

Supón que la lista empieza en A. T1 prepara el nodo X y T2 prepara el nodo Y. Sin un cerrojo compartido, ambos pueden leer head == A, asignar A al next de su nuevo nodo y después guardar su propio nodo en head. La asignación posterior de la cabecera oculta el primer nodo insertado. Ese nodo queda inaccesible, se pierde una inserción y se produce una fuga de su memoria.

La secuencia protegida debe incluir la lectura de la cabecera actual, el establecimiento del nuevo enlace y la publicación de la nueva cabecera. Un cerrojo solo alrededor de la asignación final permitiría que ambos hilos construyeran enlaces a partir de la misma cabecera desactualizada.

Prepara los datos privados antes de adquirir el cerrojo

Un nodo nuevo permanece privado hasta que se enlaza en la lista compartida. La asignación de memoria y la inicialización de la clave pueden hacerse antes de la adquisición. Usando la función auxiliar de pthread con comprobación de errores anterior, la inserción puede escribirse así:

int insert(list_t *list, int key) {
    node_t *node = malloc(sizeof(*node));
    if (node == NULL) return -1;
    node->key = key;

    check(pthread_mutex_lock(&list->lock));
    node->next = list->head;
    list->head = node;
    check(pthread_mutex_unlock(&list->lock));
    return 0;
}

Si falla la asignación de memoria, se retorna antes de adquirir ningún mutex. La sección compartida sigue siendo pequeña mientras la comprobación de head y la actualización del enlace permanecen juntas. Que la asignación de memoria sea segura entre hilos no hace que la propia lista lo sea; sus enlaces siguen necesitando su propia protección.

Los lectores participan en la misma regla

Una búsqueda adquiere el mutex de la lista antes de leer head y lo conserva durante el recorrido. Esto la coordina con la inserción y la eliminación. Asigna una variable de resultado antes del bucle, actualízala si hay una coincidencia y sal del bucle hacia un único punto de liberación antes de retornar. De lo contrario, un retorno anticipado por éxito dentro del bucle protegido necesitaría su propia liberación.

En este diseño, la eliminación posee el mismo mutex mientras desenlaza un nodo. Otros recorridos no pueden estar en ese nodo porque también necesitan el mutex. Libéralo solo cuando ninguna otra operación pueda seguir accediendo a él. Si una API permite que quien llama conserve un puntero al nodo después de liberar el cerrojo, ese uso adicional necesita su propio acuerdo sobre el tiempo de vida; el cerrojo de la lista por sí solo ya no lo cubre.

Decide si compensa usar cerrojos por nodo

El acoplamiento de cerrojos adquiere el cerrojo del siguiente nodo antes de liberar el del nodo actual. Ese solapamiento conserva una ruta protegida a medida que avanza el recorrido. Así, varias operaciones pueden ocupar distintas regiones de la lista.

La concurrencia adicional tiene un precio: cada nodo visitado añade operaciones de adquisición y liberación, incluso cuando no hay ningún otro hilo cerca. Una carga de trabajo con muchos recorridos puede ser más lenta que la versión con un solo mutex. Proteger grupos de nodos es una solución intermedia. Compara el diseño más complicado con el sencillo bajo la carga de trabajo real, incluidas las búsquedas fallidas y las rutas de eliminación.

Puntos clave

  • Protege conjuntamente la lectura de la cabecera anterior y la publicación de la nueva.
  • Asigna e inicializa los datos privados del nodo antes de adquirir el cerrojo de la lista.
  • La búsqueda y la eliminación siguen la misma regla de protección que la inserción.
  • En el acoplamiento de cerrojos, adquiere el cerrojo del siguiente nodo antes de liberar el del actual.

5.3 Separa los extremos de la cola y las cubetas de la tabla hash

Un mutex por estructura es un punto de partida sensato, pero puede serializar operaciones que acceden a distintas regiones. Dos mejoras útiles son un cerrojo por extremo de la cola y un cerrojo por cubeta de la tabla hash.

Mantén un nodo ficticio al principio de la cola

Una cola con dos cerrojos empieza con un nodo ficticio D y head == tail == D. D no contiene ningún elemento actual de la cola. El primer elemento real, si existe, está en head->next. Un mutex del principio serializa las extracciones; un mutex del final serializa las inserciones. Por tanto, una inserción y una extracción pueden avanzar juntas.

La inserción prepara de forma privada el valor de un nodo nuevo y su enlace siguiente nulo. Bajo el mutex del final, publica ese nodo mediante el campo next del último nodo anterior y mueve tail al nuevo nodo. La extracción usa el mutex del principio para leer el enlace siguiente del nodo ficticio anterior. Si es nulo, libera el cerrojo e indica que la cola está vacía. De lo contrario, copia el primer valor real, mueve head a ese nodo, libera el cerrojo y libera el nodo ficticio anterior.

El nodo que contenía el valor devuelto se convierte en el siguiente nodo ficticio. Liberar ese nodo inmediatamente dejaría head como puntero colgante.

Sigue el desplazamiento del nodo ficticio

Empieza con D. Insertar 5 crea N5 y enlaza D -> N5, con tail == N5. Insertar 8 añade N8 y produce D -> N5 -> N8. Una extracción devuelve 5, mueve head a N5 y libera D. N5 es ahora el nodo ficticio; solo queda 8 como valor en la cola. Después de otra extracción, N8 se convierte en el nodo ficticio y la cola queda vacía.

Los dos extremos siguen compartiendo un enlace

Cuando la cola está vacía, una inserción puede escribir en next del nodo ficticio actual mientras una extracción lo lee bajo un mutex distinto. Los dos mutex por sí solos no sincronizan esos accesos. En C11, declara next como puntero atómico y publícalo y léelo con la ordenación adecuada; bastan los almacenamientos y las cargas atómicas predeterminadas con consistencia secuencial. La publicación debe hacer visible el valor preparado del nodo antes de que la extracción lo lea.

Esperar a un elemento requiere un mecanismo de señalización además del cerrojo.

Aplica hash a la clave y después adquiere el cerrojo de la cubeta

Una tabla hash de tamaño fijo puede usar una lista segura entre hilos en cada cubeta, cada una con su propio mutex. Con 101 cubetas y bucket = key % 101, las claves 7 y 50 usan cerrojos distintos. Las claves 7 y 108 se asignan ambas a la cubeta 7 y deben serializarse allí. Las colisiones afectan tanto a la contención como a la longitud de la búsqueda.

Todos los accesos a la lista de una cubeta siguen la regla de protección de esa cubeta, incluidas las lecturas. El redimensionamiento cambia la asignación de cubetas y mueve entradas, por lo que este diseño sencillo necesita acceso exclusivo a toda la tabla durante el redimensionamiento. Un mutex de cubeta por sí solo no puede proteger un cambio que afecta a todas las cubetas.

Mejora el diseño solo después de encontrar el cuello de botella

Empieza con un mapa del estado compartido: qué campos se comparten, qué mutex u operación atómica protege cada uno y cuándo los nodos pasan a ser accesibles o pueden liberarse. Comprueba todas las rutas de retorno y de fallo de asignación. Razona sobre los intercalados, prueba bajo carga con un detector de carreras y mide la contención antes de añadir cerrojos de grano más fino.

Puntos clave

  • Un nodo ficticio permite que las operaciones en el principio y el final de la cola usen cerrojos separados.
  • Una extracción exitosa libera el nodo ficticio anterior y convierte el nodo del valor en el nuevo nodo ficticio.
  • El enlace next compartido necesita publicación y lectura atómicas en una cola con dos cerrojos de C11.
  • Los cerrojos de cubeta separados permiten que se solapen operaciones en distintas cubetas.
  • El redimensionamiento de una tabla hash requiere coordinación en toda la tabla.
  • Mejora un diseño correcto usando mediciones de contención y comprobando los tiempos de vida de la memoria.

Ejemplos de Hilos y cerrojos

5 Lecciones Interactivas

1

Sigue a un trabajador desde su creación hasta su terminación

14 pasos
Separa la memoria compartida del estado de los hilos, pasa argumentos estables y recoge los resultados después de esperar a su terminación.
  1. 1
    IntroducciónEmpieza con dos tipos de estado

    Un programa puede tener varios hilos que avanzan por su código. Cada ejecución necesita su propia instrucción actual y sus propios cálculos sin terminar, mientras que todos los hilos pueden acceder a los mismos objetos del programa.

    Sigue primero esos dos tipos de estado. Después construye una suma con dos trabajadores y decide cuándo el hilo principal puede leer los resultados. La seguridad de ese diseño depende de los tiempos de vida de los objetos y de la terminación, no de adivinar qué trabajador se ejecuta primero.

  2. 2
    ExplicaciónDa a cada ejecución su propio lugar

    Un hilo tiene su propio contador de programa, registros, pila e identificador de hilo. Su pila almacena los marcos de llamadas a funciones y las variables locales ordinarias. El sistema operativo guarda el estado de ejecución en un TCB, o bloque de control de hilo.

    Cuando el planificador cambia de hilo, restaura el estado del nuevo hilo. Un cálculo en el registro del primer hilo permanece sin terminar hasta que ese hilo reanuda su ejecución.

  3. 3
    EjemploEncuentra los objetos compartidos

    Supón que total es una variable global y que cada trabajador declara una variable local i. Todos los trabajadores acceden a un único total compartido, pero cada llamada a la función tiene su propio i.

    Los hilos también comparten el montículo y los descriptores de archivo abiertos. Sus pilas están en distintas direcciones dentro de ese mismo espacio de direcciones del proceso. Pasar la dirección de la variable local de un trabajador permite que otro hilo acceda a ella; las pilas separadas no garantizan el aislamiento.

  4. 4
    ConexiónSolapa el trabajo, incluso en un núcleo

    En un núcleo de CPU, el planificador intercala los hilos: avanzan de forma concurrente, pero no se ejecutan simultáneamente. En varios núcleos pueden ejecutarse en paralelo.

    Los hilos pueden acelerar cálculos independientes o solapar la espera de entrada/salida con trabajo útil. Un cambio dentro del mismo proceso conserva el espacio de direcciones y las tablas de páginas, por lo que suele costar menos que un cambio de proceso. La memoria compartida también permite que una escritura no válida dañe los datos de otros hilos.

  5. 5
    ExplicaciónObserva qué puede planificar el núcleo

    En un modelo uno a uno, cada hilo de aplicación es un hilo planificado por el núcleo. El planificador puede colocarlos en distintos núcleos y una llamada al sistema bloqueante detiene al hilo que la realiza. Linux y Windows usan este modelo para los hilos ordinarios.

    Una biblioteca muchos a uno ejecuta hilos de usuario sobre un hilo del núcleo. Esos hilos de usuario no pueden ejecutarse en varios núcleos y una llamada al sistema bloqueante puede detener a todo el grupo.

  6. 6
    Desafío¿Un núcleo adicional crea el error?

    Un trabajador se detiene después de cargar datos compartidos y otro los cambia antes de que el primero reanude su ejecución. ¿Podría ocurrir eso en un núcleo?

    ---reveal---

    Sí. El planificador puede cambiar de hilo entre la carga y el uso posterior del valor. La ejecución en paralelo puede dar lugar a más solapamientos, pero la concurrencia por sí sola permite este orden de ejecución. Para afirmar que una ejecución en un núcleo es segura, hay que tener en cuenta los cambios entre instrucciones máquina.

  7. 7
    ExplicaciónInicia una función con un puntero

    Incluye <pthread.h> y compila con -pthread. Una función trabajadora tiene la firma void *worker(void *arg). pthread_create(&t, NULL, worker, arg) la inicia, solicita los atributos predeterminados y guarda su identificador de hilo en t.

    La llamada devuelve 0 si tiene éxito o directamente un número de error. Comprueba ese resultado antes de considerar creado el hilo. El nuevo hilo puede ejecutarse inmediatamente; la inicialización ya debe estar completa.

  8. 8
    EjemploDivide la suma en trabajos separados

    Prepara dos estructuras que contengan lo, hi y result: un rango es 1–50 y el otro es 51–100. Pasa la dirección de una estructura distinta a cada trabajador.

    Cada trabajador suma su rango en un acumulador local privado y después escribe en su propio campo de resultado. El primero obtiene 1275 y el segundo 3775. Ningún trabajador escribe en el resultado del otro, por lo que el diseño evita actualizaciones concurrentes de un mismo acumulador.

  9. 9
    EjemploEspera antes de leer las respuestas

    Crea ambos trabajadores antes de esperar a la terminación de cualquiera de ellos, para que sus cálculos puedan solaparse. Después el hilo principal llama a pthread_join por cada creación exitosa. La espera a la terminación aguarda a que termine el hilo indicado y hace visibles sus escrituras para el hilo que espera.

    El hilo principal puede ahora leer los dos campos de resultado separados y sumarlos para obtener 5050. Esperar a un solo trabajador no establecería que el otro hubiera terminado su propio resultado.

  10. 10
    ConsejoMantén vivos los argumentos y resultados

    Un puntero de argumento no prolonga el tiempo de vida de un objeto. Mantén en memoria las estructuras del creador hasta que terminen los trabajadores. Un puntero devuelto puede referirse a memoria del montículo o a almacenamiento adecuado que pertenezca al creador; no puede apuntar de forma segura a una variable local de un trabajador que ya ha terminado.

    En un bucle de creación, pasa elementos distintos y estables de un arreglo en lugar de pasar repetidamente &i. Esta última opción apunta a todos los trabajadores a la misma variable de bucle cambiante.

  11. 11
    Desafío¿Qué resultado está listo?

    Dos trabajadores escriben en campos de resultado separados. El hilo principal espera correctamente a la terminación del trabajador A y después lee ambos campos mientras el trabajador B aún puede estar ejecutándose. ¿Es segura toda esa lectura?

    ---reveal---

    El resultado de A está listo y visible después de la espera a su terminación. El campo de B todavía puede estar cambiando, por lo que el hilo principal también debe esperar a que termine B antes de leerlo en este diseño. El almacenamiento separado evita interferencias entre trabajadores; esperar a la terminación establece la transferencia de cada trabajador al hilo principal.

  12. 12
    ConsejoDistingue terminar un hilo de terminar su proceso

    Retornar de main o llamar a exit() termina todo el proceso, incluidos los trabajadores que no hayan terminado. El hilo principal normalmente espera a los trabajadores necesarios antes de retornar; pthread_exit() solo termina el hilo que llama.

    Un trabajador separado recupera automáticamente sus recursos de hilo cuando termina y no se puede esperar a su terminación. Separarlo no mantiene vivo el proceso ni da al trabajador una copia privada de los objetos de argumentos del creador.

  13. 13
    DesafíoTermina el programa en el momento adecuado

    El hilo principal inicia dos trabajadores que leen su arreglo local, separa uno y espera a la terminación del otro. El trabajador separado sigue ejecutándose. ¿Le permite su pila separada terminar de usar el arreglo después de que el hilo principal retorne?

    ---reveal---

    No. Retornar de main termina todo el proceso, incluido ese trabajador. Su pila separada tampoco copia el arreglo: el puntero accede al objeto de main en su espacio de direcciones compartido. Esperar a un trabajador solo establece la terminación de ese trabajador.

  14. 14
    RepasoComprueba la transferencia del trabajador

    Para un diseño con trabajadores, localiza su estado de ejecución, los objetos que comparten y el tiempo de vida de cada argumento y resultado. Después identifica el evento que deja un resultado listo para quien lo lee.

    Los objetos de resultado distintos junto con las esperas a la terminación pueden evitar por completo los cerrojos. Las actualizaciones compartidas mientras los trabajadores se solapan necesitan coordinación adicional. El orden de creación, las pilas separadas y la separación de hilos responden cada uno a una pregunta distinta; ninguno establece que otro hilo haya completado sus escrituras.

2

Sigue la operación que necesita protección

11 pasos
Sigue una actualización perdida, distingue dos tipos de carrera y protege una decisión junto con su acción.

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

  1. 1
    IntroducciónDescompón la operación compartida
  2. 2
    ExplicaciónSepara la memoria de los registros privados
  3. 3
    EjemploDetente antes del almacenamiento
  4. 4
    EjemploReanuda con el resultado desactualizado
  5. 5
    DesafíoCambia el punto de planificación
  6. 6
    ConsejoElige una corrección mediante sincronización
  7. 7
    ExplicaciónDistingue los dos tipos de carrera
  8. 8
    EjemploAprueba una retirada dos veces
  9. 9
    Idea claveIdentifica la sección completa
  10. 10
    DesafíoSigue juntas la aprobación y la disminución
  11. 11
    RepasoEncuentra el límite antes de elegir el cerrojo
3

Revisa una regla de protección con mutex

14 pasos
Usa un cerrojo compartido, elige su alcance y sigue todas las rutas de éxito y error hasta su liberación.

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

  1. 1
    IntroducciónHaz visible el acuerdo
  2. 2
    ExplicaciónInicializa antes de publicar el objeto
  3. 3
    EjemploColoca dentro toda la actualización
  4. 4
    DesafíoInterpreta el resultado de un intento de adquisición sin espera
  5. 5
    ConsejoElige el tipo de mutex de forma deliberada
  6. 6
    ExplicaciónPermite que las regiones independientes se solapen
  7. 7
    EjemploCalcula el límite de la serialización
  8. 8
    DesafíoAcorta el trabajo adecuado
  9. 9
    Idea claveMide tres resultados distintos
  10. 10
    EjemploSigue el retorno anticipado
  11. 11
    ExplicaciónMantén un único cerrojo y su tiempo de vida
  12. 12
    DesafíoDetecta un ciclo de función de retorno
  13. 13
    DesafíoElige una corrección completa tras la revisión
  14. 14
    RepasoLee el código teniendo en cuenta la propiedad
4

Construye la propiedad, los turnos equitativos y una espera eficiente

21 pasos
Avanza desde las comprobaciones defectuosas de indicadores hasta los cerrojos atómicos y después compara el orden por boletos, los protocolos de suspensión y la herencia de prioridades.

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

  1. 1
    IntroducciónConstruye el cerrojo por capas
  2. 2
    EjemploDos hilos superan la misma comprobación
  3. 3
    ExplicaciónLa ordenación forma parte del algoritmo
  4. 4
    Desafío¿Protegería el control de interrupciones dos núcleos?
  5. 5
    ExplicaciónIntercambia el indicador atómicamente
  6. 6
    EjemploCrea un indicador portable para espera activa
  7. 7
    EjemploUna comparación fallida aporta información útil
  8. 8
    ExplicaciónConoce los resultados de otras operaciones atómicas
  9. 9
    DesafíoRepite la actualización, no la respuesta anterior
  10. 10
    Idea claveLa exclusión no garantiza un turno
  11. 11
    AnalogíaUsa una cola de atención numerada
  12. 12
    EjemploHaz avanzar la atención de un propietario cada vez
  13. 13
    DesafíoSepara el dispensador de la atención
  14. 14
    ExplicaciónLa cesión y la suspensión cambian estados distintos
  15. 15
    EjemploEncuentra el intervalo de despertar perdido
  16. 16
    ExplicaciónCompara y bloquea en una sola decisión
  17. 17
    DesafíoUn despertar llega antes de la espera
  18. 18
    ConexiónRealiza una breve espera activa cuando el propietario pueda terminar
  19. 19
    EjemploObserva la inversión de prioridades a través del propietario
  20. 20
    DesafíoCombina la planificación con el cerrojo
  21. 21
    RepasoPregunta qué espera el hilo en espera
5

Sigue contadores, nodos y cubetas compartidos

16 pasos
Analiza contadores por lotes, publicación de listas, el nodo ficticio móvil de la cola y cerrojos por cubeta.

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

  1. 1
    IntroducciónEncuentra el límite de lo compartido
  2. 2
    ExplicaciónDistribuye las escrituras frecuentes del contador
  3. 3
    EjemploTransfiere un lote completo
  4. 4
    Idea clavePon un límite a los incrementos que faltan
  5. 5
    DesafíoElige la actualidad del valor para una nueva carga de trabajo
  6. 6
    ConsejoDetén las transferencias para obtener una instantánea exacta
  7. 7
    EjemploPublica el nuevo nodo de lista al final
  8. 8
    ExplicaciónLos lectores y quienes eliminan comparten el cerrojo
  9. 9
    DesafíoAvanza mediante cerrojos por nodo
  10. 10
    EjemploInicializa la cola sin ningún elemento real
  11. 11
    EjemploDesplaza el nodo ficticio durante la extracción
  12. 12
    Idea claveEl caso vacío une los dos extremos
  13. 13
    DesafíoSigue el siguiente nodo ficticio
  14. 14
    ConexiónSepara las cubetas de la tabla hash y conserva la correspondencia
  15. 15
    DesafíoCombina un cerrojo de cubeta con una inserción segura
  16. 16
    RepasoMide el diseño completo

Ejercicios de Hilos y cerrojos

40 Preguntas de Práctica en 4 sets

Set 1: Tiempos de vida de los hilos y actualizaciones compartidas10 Preguntas de Práctica

Pregunta 1 de 10

Dos hilos pertenecen al mismo proceso. ¿Qué estado de ejecución conserva cada uno por separado?
  1. Su código ejecutable y su montículo
  2. Su contador de programa y sus registros
  3. Sus archivos abiertos y su identificador de proceso
  4. Sus variables globales y sus tablas de páginas
Mostrar respuestaOcultar respuesta
Respuesta: B
Cada hilo necesita su propio contador de programa y sus propios registros para poder reanudar su ejecución sin terminar. El montículo pertenece al espacio de direcciones compartido, por lo que tener rutas de ejecución separadas no da a los hilos objetos de montículo separados. Un objeto se vuelve privado por la forma en que se usa, no simplemente porque un hilo lo haya asignado.

Pregunta 2 de 10

Un servidor de un solo núcleo cambia entre dos hilos de solicitud mientras uno espera una respuesta de red. ¿Cómo debe describirse su ejecución?
  1. Paralela, sin progreso concurrente
  2. Concurrente, con ejecución paralela
  3. Secuencial, sin progreso solapado
  4. Concurrente, sin ejecución paralela
Mostrar respuestaOcultar respuesta
Respuesta: D
Las solicitudes avanzan durante periodos que se solapan, lo que es concurrencia. Un núcleo solo ejecuta un hilo en cada instante, por lo que los hilos no se ejecutan en paralelo. Solapar una espera de red con trabajo de CPU no requiere que dos hilos se ejecuten simultáneamente.

Pregunta 3 de 10

Un trabajador devuelve un puntero a un resultado asignado en memoria. ¿Qué llamada espera al hilo t y coloca ese puntero en void *result?
  1. pthread_join(t, &result)
  2. pthread_join(t, result)
  3. pthread_create(&t, NULL, result, NULL)
  4. pthread_detach(t)
Mostrar respuestaOcultar respuesta
Respuesta: A
El segundo argumento de join es la dirección de la variable que recibirá el puntero devuelto. Pasar result proporciona su valor actual de puntero en lugar de la ubicación donde recibirlo. Separar el hilo permite recuperar automáticamente los recursos, pero elimina la posibilidad de esperar a su terminación y recoger ese valor de retorno.

Pregunta 4 de 10

En un modelo de carga, suma y almacenamiento, un contador empieza en 36. A y B lo cargan, cada uno suma 1 de forma privada y después A y B almacenan en ese orden. ¿Cuál es el valor final del contador?
  1. 36
  2. 38
  3. 37
  4. 39
Mostrar respuestaOcultar respuesta
Respuesta: C
Ambos cálculos privados parten de 36, por lo que ambos almacenamientos escriben 37. El segundo almacenamiento sobrescribe el mismo valor; no realiza otra suma en la memoria compartida. Un valor final de 38 requeriría que un hilo cargara después de que el otro hubiera almacenado, lo que este orden de ejecución descarta.

Pregunta 5 de 10

Un saldo atómico empieza en 90. Dos hilos lo leen atómicamente, aprueban cada uno una solicitud de 60 y después cada uno realiza una resta atómica de 60. ¿Qué fallo sigue siendo posible?
  1. Se pierde una aprobación y queda un saldo de 30.
  2. Los accesos atómicos forman una carrera de datos en C.
  3. Ambos aprueban y queda un saldo de −30.
  4. Ambas restas se combinan en una sola actualización.
Mostrar respuestaOcultar respuesta
Respuesta: C
Ambas comprobaciones pueden ver 90 antes de cualquiera de las restas, por lo que se aprueban las dos solicitudes y las dos restas atómicas dejan −30. Las operaciones atómicas evitan una carrera de datos y conservan ambas restas. No mantienen válida la comprobación anterior del saldo hasta que se complete la acción que depende de ella.

Pregunta 6 de 10

Una llamada a pthread_mutex_trylock(&m) devuelve EBUSY. ¿Qué propiedad obtiene el hilo que llama mediante esa llamada?
  1. No ha adquirido la propiedad de m.
  2. Posee m hasta que vuelva a bloquearse.
  3. Comparte m con el propietario actual.
  4. Posee m cuando lo libera el propietario.
Mostrar respuestaOcultar respuesta
Respuesta: A
El retorno que indica que está ocupado significa que el intento de adquisición no tuvo éxito, por lo que quien llama no tiene la propiedad y no puede entrar en la sección protegida. El intento de adquisición sin espera no reserva una transferencia futura. El hilo puede realizar trabajo independiente o volver a intentar la adquisición más tarde.

Pregunta 7 de 10

Una función inicia dos trabajadores pasando repetidamente &i, la dirección de su índice de bucle local, y después retorna mientras se ejecutan. El hilo principal quiere resultados separados de ambos. ¿Qué diseño alternativo hace seguros el paso de argumentos y la recogida de resultados?
  1. El hilo principal mantiene viva una estructura de trabajo cambiante, inicia ambos trabajadores y espera a cada uno antes de leer su resultado.
  2. La función mantiene dos estructuras de trabajo locales, inicia ambos trabajadores, retorna y el hilo principal espera después antes de leer.
  3. El hilo principal mantiene vivas dos estructuras de trabajo distintas, inicia ambos trabajadores y lee los resultados antes de esperar a cualquiera.
  4. El hilo principal mantiene vivas dos estructuras de trabajo distintas, inicia ambos trabajadores y espera a cada uno antes de leer su resultado.
Mostrar respuestaOcultar respuesta
Respuesta: D
Cada trabajador necesita un objeto de argumento distinto y estable, y ambos objetos deben seguir existiendo mientras los trabajadores los usen. Después, el hilo principal debe esperar a que termine cada trabajador antes de leer el resultado que todavía pueda estar escribiendo. Esperar a la terminación no puede reparar un objeto local cuyo tiempo de vida ya terminó ni evitar que dos trabajadores reciban el mismo argumento cambiante.

Pregunta 8 de 10

Un programa pierde incrementos compartidos en un núcleo. ¿Por qué añadir volatile al contador puede no solucionarlo?
  1. El calificador volatile obliga a todos los hilos a compartir registros.
  2. La carga y el almacenamiento aún pueden separarse por un cambio de hilo.
  3. El contador pasa a ser privado del hilo que se ejecuta en ese momento.
  4. El calificador pospone los cambios de hilo hasta después de una carga.
Mostrar respuestaOcultar respuesta
Respuesta: B
Volatile no combina una lectura, un cálculo y una escritura en una actualización atómica ni sincroniza los hilos. Un cambio de contexto todavía puede situar otra actualización entre la carga y el almacenamiento. Los registros del contador son privados de cada hilo, que es precisamente por lo que el hilo que reanuda su ejecución puede conservar un resultado desactualizado.

Pregunta 9 de 10

Un valor de capacidad empieza en 7. Las solicitudes de 3, 5 y 2 lo comprueban cada una bajo un mutex antes de que se produzca cualquier disminución, por lo que todas se aprueban. Después disminuyen el valor bajo ese mutex en el mismo orden. ¿Qué valor final resulta y qué límite del mutex corrige la regla?
  1. Final 2; mantener intervalos de propiedad separados para comprobar y disminuir.
  2. Final −3; mantener la propiedad durante cada comprobación y su disminución condicional.
  3. Final −3; mantener intervalos separados, pero hacer también atómico cada acceso.
  4. Final −1; mantener la propiedad solo durante cada disminución ya aprobada.
Mostrar respuestaOcultar respuesta
Respuesta: B
Las tres solicitudes se aprueban a partir de 7, por lo que sus disminuciones posteriores dejan 4, después −1 y luego −3. Mantener la propiedad de forma continua durante cada comprobación y su disminución dependiente evita las aprobaciones desactualizadas. Los accesos individuales atómicos o las restas ya protegidas no cierran el intervalo entre la decisión y la acción.

Pregunta 10 de 10

T1 posee un mutex mientras cambia dos campos relacionados y después se le expropia la CPU. T2 llama a pthread_mutex_lock sobre el mismo mutex. ¿Qué ocurre con el acceso protegido?
  1. T2 espera a adquirirlo; T1 puede reanudar y terminar su actualización.
  2. T2 entra porque T1 dejó de ejecutarse en el procesador.
  3. T2 adquiere la propiedad y completa la actualización pendiente de T1.
  4. T1 pierde la propiedad y debe reiniciar los cambios de ambos campos.
Mostrar respuestaOcultar respuesta
Respuesta: A
La propiedad se conserva durante la expropiación, por lo que T2 no puede entrar simplemente porque T1 no esté usando la CPU. T1 puede reanudar su ejecución con el mutex todavía adquirido, completar la actualización de los dos campos y liberarlo. Un mutex controla la entrada al código protegido; no exige que su propietario se ejecute sin interrupción.
Set 2: Límites de los mutex y propiedad atómica10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

Un mutex protege todas las cubetas de una tabla. ¿Qué tipo de granularidad de cerrojos es esta?
  1. Uso de cerrojos de grano fino
  2. Acoplamiento de cerrojos
  3. Uso de cerrojos de grano grueso
  4. Uso de cerrojos por núcleo
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Una búsqueda en una lista adquiere su mutex, retorna inmediatamente si encuentra una coincidencia y libera el cerrojo solo después del bucle si no la encuentra. ¿Por qué pueden detenerse operaciones posteriores?
  1. Una búsqueda exitosa retorna conservando el mutex.
  2. Una búsqueda sin éxito libera el mutex antes de leer la cabecera.
  3. Una búsqueda exitosa destruye automáticamente el mutex del nodo.
  4. Un resultado devuelto transfiere la propiedad del mutex a quien llama.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

En un protocolo con indicador, A observa que el indicador está desactivado y se detiene antes de establecerlo. B lo observa desactivado, lo establece y entra. ¿Qué puede ocurrir cuando A reanuda su ejecución?
  1. A debe volver a comprobar el indicador antes de su asignación pendiente.
  2. A establece el indicador y también entra, violando la exclusión.
  3. La asignación de B cambia el valor que A ya observó.
  4. El cambio de contexto de A cancela su comprobación de disponibilidad completada.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un intercambio atómico guarda 1 en un indicador que actualmente contiene 0. ¿Qué valor devuelve comprobar y establecer?
  1. 1
  2. La dirección del indicador
  3. El boleto del hilo
  4. 0
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un servicio de 16 núcleos usa un mutex por cuenta. Una transferencia adquiere primero el mutex de origen y después el de destino. Las transferencias concurrentes pueden ir en sentidos opuestos. ¿Qué regla permite que las cuentas independientes sigan operando de forma concurrente y corrige el problema de orden de los cerrojos?
  1. Adquirir primero el de origen y liberarlo antes de adquirir el de destino.
  2. Adquirir primero el de destino en cada transferencia en ambos sentidos.
  3. Adquirir un mutex global para cada operación sobre cualquier cuenta.
  4. Adquirir ambos mutex de cuenta en orden creciente de identificador de cuenta.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Un bucle CAS lee un contador con valor 12 y prepara 13. Otro hilo cambia el contador a 13 antes del CAS. ¿Cómo conserva el primer hilo su incremento?
  1. Repetir el mismo intento esperando 12 hasta que tenga éxito.
  2. Reintentar desde el nuevo valor del contador e intentar guardar 14.
  3. Guardar el 13 preparado sin volver a comprobar el contador.
  4. Tratar el intento fallido como un incremento ya completado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Un cerrojo por boletos tiene turn == 6 y el siguiente boleto sin usar es ticket == 9. Un nuevo hilo toma un boleto. ¿Qué número espera?
  1. 9
  2. 6
  3. 8
  4. 10
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Unos trabajadores copian una estructura de registro que contiene un mutex y un puntero a registros compartidos. Adquieren sus copias, retornan antes de tiempo si falla la validación y leen el estado usando mutex locales separados. ¿Qué corrección establece un único acuerdo de protección y libera la propiedad en caso de fallo?
  1. Usar el registro original mediante un puntero, proteger las escrituras con su mutex y mantener cerrojos locales privados para los lectores.
  2. Inicializar cada mutex copiado, proteger lecturas y escrituras con esa copia y dirigir los fallos a su liberación.
  3. Usar el registro original mediante un puntero, proteger lecturas y escrituras con su mutex y dirigir los fallos a su liberación.
  4. Usar el registro original mediante un puntero, proteger lecturas y escrituras con su mutex y retornar directamente si hay un fallo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Dos hilos ejecutan el protocolo de Peterson. Las escrituras en sus propios indicadores permanecen en búfer, por lo que ambas lecturas posteriores ven al otro como no interesado. Solo turn es atómico; los indicadores son enteros ordinarios de C. ¿Qué conclusión y corrección se derivan?
  1. Solo uno puede entrar; un turno atómico por sí solo conserva todo el protocolo.
  2. Ambos pueden entrar; usar accesos atómicos ordenados correctamente para los indicadores y el turno.
  3. Ambos pueden entrar; marcar solo los indicadores ordinarios como volatile corrige el orden.
  4. Solo uno puede entrar; las líneas de caché separadas eliminan las carreras de datos de los indicadores ordinarios.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Cada actualización de un par de campos compartidos usa el mutex m, pero una función de diagnóstico lee ambos campos sin m. ¿Qué debe hacer la función de diagnóstico para obtener un par protegido y consistente?
  1. Declarar volatile sus copias locales.
  2. Mantener un mutex privado durante las lecturas.
  3. Poseer m mientras lee ambos campos.
  4. Leer el primer campo dos veces sin bloqueo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 3: Protocolos de espera y estructuras compartidas10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

Un hilo que espera un cerrojo está suspendido en una cola de espera del núcleo. ¿Qué tiempo de CPU consume mientras permanece bloqueado?
  1. Un intervalo completo en cada ciclo del planificador.
  2. Un núcleo continuamente hasta que el propietario lo libera.
  3. Una breve espera activa en cada acceso protegido a memoria.
  4. Ninguno para ejecutar ese hilo bloqueado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

A posee el boleto 20; B y C poseen el 21 y el 22. A libera el cerrojo, B deja de ejecutarse y C sigue esperando mientras se ejecuta. D toma ahora el siguiente boleto, sin más llegadas. ¿Cuál es el siguiente boleto sin usar y qué orden de entrada debe seguir una vez que B pueda ejecutarse?
  1. Siguiente sin usar 23; B, después C y después D.
  2. Siguiente sin usar 24; C, después D y después B.
  3. Siguiente sin usar 24; B, después C y después D.
  4. Siguiente sin usar 22; B, después D y después C.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Un hilo en espera decide suspenderse, se detiene y recibe un unpark antes de llamar a park. El unpark se descarta. ¿Qué riesgo resulta de ello?
  1. Puede suspenderse después de que ya haya pasado el único despertar.
  2. Puede adquirir dos boletos para la misma entrada de la cola.
  3. Debe realizar espera activa continua hasta el siguiente intervalo.
  4. Se despierta de inmediato porque park siempre recuerda unpark.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un contador aproximado usa tres posiciones locales y el umbral 4. Partiendo de cero, sus totales de actualizaciones son 6, 1 y 3. Después de que terminen todas las actualizaciones, ¿cuáles son el recuento global y el total real?
  1. Global 8; total real 10.
  2. Global 4; total real 10.
  3. Global 6; total real 10.
  4. Global 4; total real 6.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Antes de enlazar un nodo de lista recién asignado a cualquier estructura compartida, ¿qué trabajo puede hacerse sin el mutex de la lista?
  1. Leer la cabecera compartida para actualizar un enlace después.
  2. Cambiar la cabecera compartida para publicar el nuevo nodo.
  3. Inicializar el campo de clave del nodo privado.
  4. Recorrer nodos compartidos que una eliminación puede liberar.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una cola contiene el nodo ficticio R seguido de nodos que contienen 14 y 29. Después de una extracción exitosa, ¿qué nodo se libera y cuál se convierte en el nodo ficticio?
  1. Se libera el nodo que contiene 14; R sigue siendo el ficticio.
  2. Se libera R; el nodo que contiene 14 se convierte en el ficticio.
  3. Se libera R; el nodo que contiene 29 se convierte en el ficticio.
  4. Se libera el nodo que contiene 29; R sigue siendo el ficticio.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Una tabla hash de tamaño fijo usa 17 cubetas, key % 17 y un mutex por cubeta. Los trabajadores actualizan las claves 8, 25 y 26. ¿Qué par debe serializarse y qué clave usa un cerrojo separado?
  1. 8 y 25 se serializan; 26 usa un cerrojo separado.
  2. 8 y 26 se serializan; 25 usa un cerrojo separado.
  3. 25 y 26 se serializan; 8 usa un cerrojo separado.
  4. Las tres se serializan; cada una usa el mismo cerrojo de cubeta.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un contador aproximado tiene cuatro posiciones locales e incrementos de una unidad. Su retraso después de completar las actualizaciones nunca debe superar 20. Elige el mayor umbral entero que cumpla ese límite. Después, las posiciones realizan 17, 8, 5 y 0 actualizaciones partiendo de cero. ¿Qué umbral y lectura global resultan?
  1. Umbral 5; lectura global 25.
  2. Umbral 7; lectura global 21.
  3. Umbral 6; lectura global 30.
  4. Umbral 6; lectura global 18.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un recorrido con acoplamiento de cerrojos posee el cerrojo del nodo actual. ¿En qué orden debe avanzar al siguiente nodo?
  1. Liberar el cerrojo actual y después adquirir el siguiente.
  2. Liberar el cerrojo actual y después copiar el puntero siguiente.
  3. Adquirir todos los cerrojos de nodos posteriores antes de liberar alguno.
  4. Adquirir el cerrojo siguiente y después liberar el actual.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

En una CPU con planificación estricta por prioridades, L, de baja prioridad, posee un cerrojo; H, de alta prioridad, lo espera activamente; y M, de prioridad media, está listo. Primero se cambia el cerrojo por uno de espera activa por boletos. Después se sustituye por un mutex con herencia de prioridades en el que H se bloquea. ¿Qué progreso permite el segundo cambio que el orden por boletos por sí solo no permite?
  1. L se ejecuta antes que M, libera el cerrojo y permite avanzar a H; el orden por boletos deja que H impida ejecutar a L.
  2. H entra antes que L, libera el cerrojo y permite avanzar a M; el orden por boletos exige que M lo libere primero.
  3. M se ejecuta antes que L, libera el mutex de H y permite avanzar a H; el orden por boletos bloquea igual todas las prioridades.
  4. L permanece por debajo de M hasta que M termina; el orden por boletos elevaría a L a la prioridad de H.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Set 4: Revisa diseños concurrentes entre límites10 Preguntas de Práctica

Guarda esta unidad para ver todas las respuestas y practicar cada set.

Pregunta 1 de 10

Nunca se recogerá el valor de retorno de un trabajador cuya terminación se puede esperar, y debe recuperar sus recursos de hilo cuando termine. ¿Qué operación de pthread dispone esto?
  1. Llamar a pthread_join sin esperarlo.
  2. Separar al trabajador con pthread_detach.
  3. Retornar de main antes de que termine el trabajador.
  4. Copiar el pthread_t del trabajador localmente.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

W observa la palabra de un cerrojo de futex en 1. El propietario la pone en 0 y despierta antes de que W llame a la espera. Después C lo adquiere y la pone en 1. W llama a la espera de futex esperando 1, y C más tarde la pone a cero y despierta. ¿Puede W bloquearse en esa llamada y el despertar posterior le concede la propiedad?
  1. W puede bloquearse por el estado ocupado de C; al despertar debe reintentar la adquisición.
  2. W no puede bloquearse porque un propietario anterior ya envió una notificación de despertar.
  3. W puede bloquearse por el estado ocupado de C; el despertar posterior le concede la propiedad automáticamente.
  4. W debe bloquearse para siempre porque su lectura anterior se refería a otro propietario.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Una cola con dos cerrojos está vacía en el nodo ficticio D. E prepara el nodo R con valor 11, publica D->next = R y se detiene antes de mover el final. Después Q realiza una extracción exitosa; a continuación, E termina de mover el final a R. ¿Qué estado queda y cómo deben implementarse en C11 los accesos a next entre los extremos?
  1. Un elemento 11 en R; publicar y leer next atómicamente con ordenación adecuada.
  2. Vacía con R ficticio; los accesos ordinarios a next se ordenan mediante los mutex separados de los extremos.
  3. Vacía con D ficticio; los accesos volatile a next proporcionan el orden de publicación necesario.
  4. Vacía con R ficticio; publicar y leer next atómicamente con ordenación adecuada.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Cada solicitud posee un mutex compartido durante 5 µs. Sin tener en cuenta los demás costes, ¿qué límite superior de tasa de procesamiento impone ese trabajo protegido?
  1. 5000 solicitudes por segundo.
  2. 1 000 000 de solicitudes por segundo.
  3. 200 000 solicitudes por segundo.
  4. 5 000 000 de solicitudes por segundo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un contador tiene una posición local en 5 y el valor global en 40. Un lector copia el valor global bajo su cerrojo y lo libera. Un trabajador transfiere el 5 local al global y restablece el local. Después el lector copia el valor local bajo su cerrojo. ¿Qué total calcula el lector y cómo puede obtener una instantánea exacta?
  1. Calcula 40; poseer todos los cerrojos locales y después el global para la instantánea.
  2. Calcula 45; leer el global dos veces sin adquirir ningún cerrojo local.
  3. Calcula 50; proteger solo el contador global durante toda la instantánea.
  4. Calcula 40; proteger solo cada campo brevemente mientras se copia.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una actualización de caché posee un mutex normal y llama a una función de retorno que consulta la misma caché. Trasladar la función de retorno después de liberar el cerrojo evitaría la reentrada, pero otro trabajador podría entonces liberar el nodo al que apunta su puntero directo. ¿Qué rediseño permite llamar a la función de retorno fuera del cerrojo y mantener válida su entrada?
  1. Conservar el puntero directo al nodo, liberar el cerrojo y dejar que la función de retorno adquiera el mismo mutex al comenzar.
  2. Copiar los campos necesarios en almacenamiento de la función de retorno bajo el mutex, liberar el cerrojo y llamarla con esa copia.
  3. Copiar los campos en almacenamiento de la función de retorno, mantener el mutex adquirido y dejar que consulte mediante el mismo mutex.
  4. Conservar el puntero directo al nodo, liberar el cerrojo y marcar el puntero como volatile para que una eliminación posterior no invalide el nodo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Un almacenamiento condicional falla después de una carga enlazada aunque el código no tiene indicios de otro escritor. ¿Cómo debe responder la rutina de actualización?
  1. Tratar el almacenamiento condicional fallido como exitoso.
  2. Escribir el valor con un almacenamiento ordinario incondicional.
  3. Reintentar la secuencia LL/SC con una nueva carga enlazada.
  4. Devolver el valor anterior e informar de que se confirmó la actualización.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Dos búsquedas encuentran un nodo. A copia su valor entero en almacenamiento de quien llama bajo el mutex de la lista. B guarda un puntero y libera el cerrojo. Después, una operación de eliminación desenlaza y libera el nodo; su dirección puede reutilizarse antes de que B lea. ¿Qué resultado sigue siendo válido y qué debe proporcionar la interfaz de B?
  1. Ambos resultados siguen siendo válidos; B solo necesita adquirir de nuevo el cerrojo de la lista antes de leer la dirección guardada.
  2. Solo el puntero de B sigue siendo válido; A debe adquirir el cerrojo de la lista siempre que lea el valor copiado.
  3. El valor copiado de A sigue siendo válido; marcar como volatile el puntero guardado de B conserva el nodo eliminado.
  4. El valor copiado de A sigue siendo válido; B necesita una copia del valor o protección del tiempo de vida continuado del nodo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

El siguiente boleto sin usar de un cerrojo por boletos es 15 y su turno actual es 12. Tres hilos toman boletos antes de cualquier liberación. ¿Cuáles son sus boletos y el nuevo valor del dispensador?
  1. Boletos 15, 16, 17; siguiente sin usar 18.
  2. Boletos 12, 13, 14; siguiente sin usar 15.
  3. Boletos 16, 17, 18; siguiente sin usar 18.
  4. Boletos 15, 15, 15; siguiente sin usar 18.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Una tabla crece de 11 a 17 cubetas usando la clave módulo el número de cubetas. Un trabajador ha seleccionado la cubeta anterior para la clave 24. La operación de redimensionamiento adquiere solo el cerrojo de su nueva cubeta de destino, mueve la entrada y libera el arreglo anterior de cubetas. ¿Qué destino y regla de transición preservan este diseño sencillo?
  1. Cubeta 2; mantener la correspondencia anterior mientras se publica el nuevo número de cubetas para las operaciones ordinarias.
  2. Cubeta 7; proteger solo el nuevo destino porque el trabajador calculó antes su índice anterior.
  3. Cubeta 7; excluir las operaciones ordinarias de la tabla y esperar a que terminen los usuarios del arreglo anterior antes de liberarlo.
  4. Cubeta 2; proteger solo el origen anterior mientras se liberan todas las cubetas anteriores después de mover la entrada.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

¿Te atascas con una pregunta tuya? Ayuda con Tareas

Términos Clave

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

A

Acoplamiento de cerrojos
El acoplamiento de cerrojos adquiere el cerrojo del siguiente nodo antes de liberar el actual, conservando la protección durante el recorrido.
Distintos recorridos pueden ocupar regiones separadas de la lista, aunque cada nodo visitado añade sobrecarga de cerrojos.
Actualización perdida
Una actualización perdida ocurre cuando un hilo guarda un resultado basado en una lectura antigua y sobrescribe el efecto de una actualización de otro hilo.
Dos trabajadores pueden cargar cada uno 50 de un contador compartido y guardar 51, dejando solo un incremento visible después de dos intentos.
Algoritmo de Peterson
El algoritmo de Peterson es un protocolo de exclusión para dos hilos que usa indicadores de interés y una variable de turno, y es correcto bajo consistencia secuencial.
Cuando ambos hilos solicitan entrar, la variable de turno determina cuál de los hilos interesados espera.
Argumento de hilo
Un argumento de hilo es un dato proporcionado a un nuevo trabajador mediante el parámetro de tipo puntero de su función de entrada, que a menudo apunta a una estructura.
Un arreglo proporciona a cada trabajador un objeto de argumento distinto que sigue siendo válido hasta que terminan las esperas a los hilos.
Atomicidad
La atomicidad significa que una operación parece indivisible a los observadores pertinentes, por lo que no pueden observar su estado parcial intermedio.
Usar un mutex compartido puede hacer atómica una actualización de dos campos para los demás usuarios de ese mismo mutex.

B

Bloque de control de hilo
Un bloque de control de hilo es un registro del sistema operativo que contiene el estado de ejecución guardado de un hilo para planificarlo y reanudarlo.
En un cambio entre dos hilos, el estado de los registros del primer hilo se guarda en su bloque de control de hilo antes de restaurar el estado del segundo.

C

Calificador volatile
El calificador de C volatile marca los accesos para que se apliquen reglas especiales de observación; no proporciona atomicidad ni sincronización entre hilos.
Marcar un contador compartido como volatile no combina su carga, suma y almacenamiento en una sola actualización.
Carga enlazada y almacenamiento condicional
La carga enlazada y almacenamiento condicional combina una lectura que establece una reserva con un intento de escritura que puede fallar y debe repetirse si no tiene éxito.
Una reserva invalidada hace que el almacenamiento condicional falle en lugar de sobrescribir un cambio de otro hilo.
Carrera de datos
Una carrera de datos es un acceso conflictivo sin sincronizar a una ubicación de memoria por distintos hilos, que incluye una escritura y un acceso no atómico.
Una lectura de estado puede tener una carrera con una escritura ordinaria del contador aunque la función de estado no cambie nada.
Cerrojo de cubeta
Un cerrojo de cubeta protege las entradas de una cubeta hash y permite que las operaciones en distintas cubetas avancen bajo cerrojos diferentes.
Dos claves con el mismo índice de cubeta compiten por el mismo cerrojo aunque tengan valores distintos.
Cerrojo de dos fases
Un cerrojo de dos fases espera realizando primero una breve espera activa y después bloqueándose si el propietario no ha liberado el cerrojo durante ese intervalo.
La breve espera activa puede evitar la sobrecarga de despertar cuando la posesión es corta, sin consumir un núcleo durante posesiones largas.
Cerrojo de espera activa
Un cerrojo de espera activa es un cerrojo de exclusión mutua cuyos hilos que no consiguen adquirirlo comprueban o intentan la adquisición repetidamente mientras consumen tiempo de CPU.
Una posesión breve en otro núcleo que se está ejecutando puede hacer que la espera activa cueste menos que bloquear y despertar.
Cerrojo por boletos
Un cerrojo por boletos da a cada solicitante un boleto atómico único y le permite entrar cuando el contador de turno alcanza ese boleto.
Un hilo que posee el boleto 6 espera mientras el turno es 5, incluso si un hilo con un boleto posterior ya se está ejecutando.
Cesión voluntaria de la CPU
Una cesión voluntaria de la CPU abandona el turno actual de quien llama y lo deja listo para volver a ejecutarse cuando el planificador lo seleccione.
Muchos hilos que esperan un cerrojo y ceden la CPU pueden causar cambios de contexto repetidos antes de que el propietario reciba otro turno.
Cola con dos cerrojos
Una cola con dos cerrojos serializa las inserciones con un cerrojo del final y las extracciones con uno del principio, permitiendo que se solapen operaciones en extremos opuestos.
En una cola de dos cerrojos de C11, el enlace siguiente del nodo ficticio en una cola vacía sigue necesitando publicación y lectura atómicas.
Colisión de hash
Una colisión de hash ocurre cuando claves distintas se asignan a la misma cubeta de la tabla, haciendo que compartan una región de búsqueda y su cerrojo.
Con 13 cubetas, las claves 2 y 15 colisionan porque cada una tiene resto 2 al dividirse entre 13.
Comparar e intercambiar
Comparar e intercambiar sustituye atómicamente un valor solo cuando coincide con un valor esperado y proporciona información suficiente para detectar un fallo.
Una función que actualiza un contador vuelve a intentarlo cuando un incremento de otro hilo deja desactualizado su valor esperado.
Comprobar y establecer
Comprobar y establecer almacena atómicamente un valor nuevo y devuelve el anterior, lo que permite que un hilo adquiera un indicador libre sin dejar un hueco.
Un intercambio que recibe 0 obtiene la propiedad del indicador; los hilos que reciben 1 siguen esperando la liberación.
Concurrencia
La concurrencia es el avance de varias ejecuciones durante periodos que se solapan, incluida la ejecución intercalada en un solo núcleo de CPU.
Un temporizador puede cambiar entre trabajadores concurrentes incluso cuando la máquina solo puede ejecutar uno en cada instante.
Condición de carrera
Una condición de carrera ocurre cuando la corrección de una operación depende de los tiempos relativos de acciones concurrentes.
Dos aprobaciones protegidas por separado pueden usar ambas el saldo anterior antes de aplicar cualquiera de las retiradas.
Consistencia secuencial
La consistencia secuencial da a las operaciones un orden común que conserva el orden de programa de cada hilo y que todos los hilos observan.
La demostración de Peterson supone que los indicadores y el turno siguen esta ordenación en lugar de tener una visibilidad reordenada.
Contador aproximado
Un contador aproximado agrupa actualizaciones locales protegidas en un recuento global protegido cuya lectura puede quedar retrasada respecto al total exacto.
Un recolector de estadísticas puede tolerar que los incrementos recientes permanezcan en los contadores locales hasta alcanzar su umbral de transferencia.
Contador de turno
Un contador de turno registra el boleto al que se permite entrar actualmente en un cerrojo por boletos y avanza una vez cuando su propietario lo libera.
Tomar un boleto nuevo hace avanzar el dispensador y deja sin cambios el turno actual.
Contador por núcleo
Un contador por núcleo registra las actualizaciones en una posición asociada a un núcleo de CPU, lo que reduce las escrituras en una única ubicación compartida globalmente.
Los hilos que usan distintas posiciones locales pueden actualizar bajo distintos mutex hasta que sea necesaria una transferencia por lotes.
Contención
La contención ocurre cuando los hilos compiten por un recurso compartido, por ejemplo, al intentar adquirir un mutex que otro hilo posee.
Añadir trabajadores a un contador protegido por un cerrojo puede aumentar la espera y el tráfico de caché compartida en lugar de la tasa de procesamiento.
Creación de hilos
La creación de hilos inicia una nueva ruta de ejecución en una función proporcionada, con un puntero de argumento y atributos de hilo configurables.
El hilo principal prepara una estructura de rango antes de pasar su dirección a un trabajador de suma creado correctamente.
Cubeta de una tabla hash
Una cubeta de una tabla hash es una región de la tabla seleccionada por la correspondencia hash de una clave, que contiene las entradas asignadas a esa misma ubicación.
Módulo 101 coloca las claves 7 y 108 en la misma cubeta aunque sus valores numéricos sean distintos.

D

Despertar perdido
Un despertar perdido ocurre cuando una notificación necesaria no impide o no termina una espera, lo que puede dejar a un hilo bloqueado incorrectamente.
Un hilo en espera con un protocolo sencillo puede suspenderse después de que se descartara un unpark anterior, sin que quede una liberación que lo despierte.

E

Espacio de direcciones
Un espacio de direcciones es el conjunto de direcciones de memoria disponibles para un proceso; sus hilos comparten el código y los objetos que contiene.
Las pilas de dos trabajadores ocupan distintas regiones de un mismo espacio de direcciones, por lo que un puntero puede acceder de una a otra.
Espera a la terminación de un hilo
Una espera a la terminación de un hilo espera a que termine un hilo cuya terminación se puede esperar y hace visibles sus escrituras completadas para el hilo que ha esperado correctamente.
El hilo principal espera a que terminen dos trabajadores de suma antes de leer y combinar sus campos de resultado separados.
Espera acotada
La espera acotada limita cuántas entradas de otros hilos en la sección crítica pueden preceder a un hilo después de que haya solicitado entrar.
Los boletos anteriores pendientes limitan el número de entradas antes del turno de un hilo que espera un cerrojo por boletos.
Espera activa
La espera activa comprueba repetidamente una condición mientras permanece lista para ejecutarse y consume tiempo de CPU hasta que esa condición permite avanzar.
Un hilo en espera puede gastar todo su intervalo de tiempo asignado comprobando un indicador mientras el propietario al que se expropió la CPU no puede ejecutarse.
Exclusión mutua
La exclusión mutua es la propiedad por la que como máximo un hilo que compite ocupa una sección crítica protegida a la vez.
Un segundo hilo no puede completar la adquisición del mutex compartido mientras un primer hilo lo posee, aunque a ese propietario se le expropie temporalmente la CPU.

F

Fuga de memoria
Una fuga de memoria es almacenamiento asignado que ya no se necesita, pero no puede recuperarse porque el programa ha perdido su referencia utilizable.
Dos inserciones al principio que compiten pueden sobrescribir la cabecera y dejar inaccesible el primer nodo nuevo.
Futex
Un futex es una palabra en memoria de usuario con soporte del núcleo de Linux para comprobar atómicamente un valor esperado y bloquear, además de despertar a los hilos en espera.
Un mutex sin contención puede usar operaciones atómicas en el espacio de usuario y llamar al núcleo solo para esperar cuando hay contención.

G

Granularidad de los cerrojos
La granularidad de los cerrojos es la cantidad de estado compartido que protege un cerrojo, lo que determina qué operaciones deben serializarse entre sí.
Pasar de un mutex de tabla a mutex por cubeta permite que se solapen operaciones en distintas cubetas.

H

Herencia de prioridades
La herencia de prioridades eleva temporalmente al propietario de un cerrojo a la prioridad más alta de un hilo en espera para que pueda ejecutarse y liberarlo antes.
Elevar la prioridad del propietario impide que el trabajo de prioridad intermedia retrase indefinidamente la liberación necesaria.
Hilo
Un hilo es una secuencia de ejecución dentro de un proceso, con su propio contador de programa, registros, pila e identidad de hilo.
Un servidor puede usar un hilo para esperar una respuesta mientras otro procesa una solicitud completada.
Hilo bloqueado
Un hilo bloqueado no puede seleccionarse para ejecutarse hasta que un evento lo deje listo de nuevo, por lo que no usa CPU mientras está bloqueado.
Un hilo suspendido que espera un mutex sale del conjunto de hilos listos hasta que un despertar le permite volver a competir.
Hilo separado
Un hilo separado recupera automáticamente sus recursos de hilo cuando termina y no se puede esperar a su terminación para recoger su valor de retorno.
Un trabajador en segundo plano puede estar separado, pero retornar de main sigue terminando el proceso que lo contiene.
Hilos POSIX
Los hilos POSIX son la interfaz estandarizada de hilos que usan los programas en C en sistemas de tipo Unix para crear y coordinar hilos.
Un programa en C con trabajadores incluye <pthread.h> y se compila y enlaza con la opción -pthread.

I

Identidad del mutex
La identidad de un mutex está ligada al objeto mutex original en la dirección donde se inicializó; un mutex de pthread copiado no es una referencia válida al original.
Pasa un registro compartido mediante un puntero en lugar de copiar su estructura y usar el mutex copiado.
Inanición
La inanición es el retraso indefinido de un hilo mientras otros hilos que compiten siguen avanzando y obtienen repetidamente el recurso que necesita.
Un hilo en espera con comprobar y establecer puede perder todas las carreras después de la liberación porque el cerrojo no recuerda el orden de llegada.
Instantánea exacta
Una instantánea exacta lee valores compartidos relacionados de forma consistente en un estado protegido, impidiendo que las transferencias dividan la observación.
Una instantánea de un contador aproximado posee los cerrojos locales y el global mientras suma todos los recuentos.
Intento de adquisición sin espera
Un intento de adquisición sin espera intenta adquirir un mutex inmediatamente e indica si quien llama lo obtuvo, en lugar de esperar mientras está ocupado.
Un resultado EBUSY permite a un trabajador elegir trabajo independiente, pero no concede acceso a los campos protegidos.
Intercalado
Un intercalado es un orden en el que ocurren pasos de distintos hilos mientras se conserva el orden de ejecución pertinente dentro de cada hilo.
Colocar la actualización completa de un hilo entre la carga y el almacenamiento de otro hilo revela cómo un resultado desactualizado puede sobrescribirla.
Inversión de prioridades
La inversión de prioridades ocurre cuando un trabajo de mayor prioridad espera a un propietario de menor prioridad cuyo progreso se retrasa por otra actividad de planificación.
El trabajo de prioridad media puede impedir que un propietario de mutex de baja prioridad lo libere para un hilo en espera de alta prioridad.

L

Lectura-modificación-escritura
Una operación de lectura-modificación-escritura lee un valor almacenado, calcula un valor modificado y vuelve a escribir el resultado en esa ubicación compartida.
Un incremento ordinario de contador deja un hueco entre su carga inicial y su almacenamiento final.
Llamada reentrante
Una llamada reentrante vuelve a entrar en una operación antes de que termine su invocación anterior y puede intentar volver a adquirir un cerrojo ocupado.
Una función de retorno de registro puede consultar la misma caché mientras la actualización externa todavía posee el mutex de la caché.

M

Modelo de hilos muchos a uno
El modelo de hilos muchos a uno asigna varios hilos de usuario a un hilo del núcleo, lo que impide su ejecución simultánea en distintos núcleos.
Una llamada al sistema bloqueante puede detener a todos los hilos de usuario asignados a esa única ruta de ejecución del núcleo.
Modelo de hilos uno a uno
El modelo de hilos uno a uno asigna cada hilo de aplicación a un hilo planificado por el núcleo, lo que permite la ejecución en paralelo y el bloqueo de cada hilo por separado.
El planificador del sistema operativo puede colocar dos hilos ordinarios de Linux en núcleos separados.
Mutex
Un mutex es un cerrojo de exclusión mutua que posee un hilo a la vez y que se usa para proteger una operación o región de datos compartidos.
Todas las operaciones sobre el saldo adquieren el mismo mutex de cuenta antes de usar el campo de saldo protegido.
Mutex con comprobación de errores
Un mutex con comprobación de errores informa de errores de propiedad especificados, incluidos la readquisición por el propietario y la liberación por un hilo que no lo posee.
Durante el desarrollo, un intento de readquisición puede devolver EDEADLK en lugar de bloquear silenciosamente al mismo hilo.
Mutex recursivo
Un mutex recursivo permite adquisiciones repetidas por su propietario y solo queda disponible después del número correspondiente de liberaciones.
Dos adquisiciones exitosas por un hilo requieren dos liberaciones antes de que otro hilo pueda adquirirlo.

N

Nodo ficticio
Un nodo ficticio ancla una cola enlazada sin representar un elemento actual; su nodo siguiente, cuando existe, contiene el primer valor de la cola.
Una extracción devuelve el valor del nodo siguiente, libera el nodo ficticio anterior y convierte el nodo del valor en el nuevo nodo ficticio.

O

Obtener y sumar
Obtener y sumar añade atómicamente una cantidad a un valor almacenado y devuelve el valor que existía antes de la suma.
Un dispensador de boletos devuelve 12 a un hilo mientras hace avanzar su siguiente número disponible a 13.
Operación atómica sin cerrojos
Una operación atómica sin cerrojos proporciona su efecto atómico sin depender de que otro hilo libere un cerrojo interno.
Otros objetos atómicos pueden usar un cerrojo de biblioteca, por lo que la corrección atómica por sí sola no establece que funcionen sin cerrojos.
Orden de atención por llegada
El orden de atención por llegada atiende las solicitudes según su orden de llegada registrado, impidiendo que las llegadas registradas más tarde adelanten a las anteriores.
En un cerrojo por boletos, la llegada se registra cuando quien llama obtiene atómicamente su número de boleto.
Ordenación de las operaciones de memoria
La ordenación de las operaciones de memoria especifica restricciones sobre cómo se hacen visibles esas operaciones, incluidas las que rodean las transferencias de un cerrojo.
La liberación de un cerrojo debe mantener las escrituras protegidas antes de publicar el nuevo estado disponible.

P

Paralelismo
El paralelismo es la ejecución simultánea en distintos recursos de procesamiento, como varios hilos ejecutándose en núcleos de CPU diferentes.
Las partes independientes de una imagen pueden procesarse en paralelo cuando hay varios núcleos disponibles.
Progreso
El progreso exige que los hilos que compiten por una sección crítica libre puedan elegir a uno que entre sin una obstrucción indefinida por parte de quienes no compiten.
Un hilo que no intenta entrar no debe mantener cerrada para siempre una sección que de otro modo estaría disponible.
Propiedad del cerrojo
La propiedad del cerrojo es el derecho exclusivo obtenido mediante una adquisición exitosa, que se conserva hasta que el hilo propietario libera el mutex.
Un cambio de contexto detiene al propietario sin conceder permiso para entrar a un hilo en espera.
Publicación de nodos
La publicación de nodos hace que se pueda acceder a un nodo privado preparado mediante enlaces compartidos, para que otras operaciones puedan encontrar sus datos inicializados de forma segura.
Una inserción al principio rellena primero la clave y publica la cabecera solo mientras posee el mutex compartido de la lista.

R

Recuperación de memoria
La recuperación de memoria libera el almacenamiento asignado solo después de que ninguna operación pueda seguir accediendo al objeto que lo ocupa.
Desenlazar un nodo no basta cuando quien llama conservó su puntero y puede desreferenciarlo más tarde.
Redimensionamiento de la tabla
El redimensionamiento de la tabla cambia la organización de las cubetas de una tabla hash y reasigna las entradas, lo que requiere coordinación más allá del cerrojo ordinario de una cubeta.
Una tabla hash sencilla con cerrojos por cubeta excluye las operaciones concurrentes de la tabla mientras mueve las entradas a nuevas cubetas.
Regla de protección
Una regla de protección asigna el estado compartido a un cerrojo concreto y especifica qué accesos deben adquirir ese mismo cerrojo.
Un comentario junto a un contador puede nombrar el mutex que cada lectura y actualización concurrente debe adquirir.
Retorno cíclico del contador
El retorno cíclico del contador ocurre cuando la aritmética sin signo supera el valor máximo representable y continúa desde cero.
La igualdad de boletos sigue siendo utilizable tras el retorno cíclico solo mientras los números de boletos pendientes sigan siendo únicos.
Ruta de liberación de recursos
Una ruta de liberación de recursos es una secuencia de salida común que libera los recursos adquiridos antes de que una función retorne por éxito o por fallo.
Una búsqueda guarda su valor de retorno, sale del bucle y llega a una única liberación compartida del mutex.

S

Sección crítica
Una sección crítica es una región de código que accede a estado compartido y debe excluir las operaciones de otros hilos para preservar la corrección de la operación.
La sección crítica de una retirada empieza antes de la comprobación del saldo e incluye la resta condicional.
Seguridad entre hilos
La seguridad entre hilos significa que una estructura se comporta según su especificación cuando se intercalan operaciones de varios hilos.
Un contador necesita una regla de protección para las lecturas además de los incrementos cuando esas operaciones pueden solaparse.

T

ThreadSanitizer
ThreadSanitizer es una herramienta de análisis en tiempo de ejecución que detecta carreras de datos en ejecuciones instrumentadas e informa de los accesos conflictivos.
Una compilación de prueba que use -fsanitize=thread puede revelar una ruta de lectura que omitió el mutex de la función de actualización.
Tiempo de vida de un objeto
El tiempo de vida de un objeto es el periodo durante el cual un objeto almacenado existe y puede accederse a él mediante una referencia que sea válida por lo demás.
Devolver un puntero a una variable local de un trabajador falla porque ese objeto deja de existir al terminar la función.

U

Umbral de transferencia
Un umbral de transferencia es el recuento local al que un contador aproximado traslada sus incrementos acumulados al recuento global.
Elevar el umbral reduce las adquisiciones del cerrojo global, pero permite que más incrementos queden sin comunicar.
Uso de cerrojos de grano fino
El uso de cerrojos de grano fino asigna cerrojos separados a regiones más pequeñas del estado compartido, permitiendo que operaciones independientes avancen de forma concurrente.
Los mutex de cuenta separados permiten actualizar al mismo tiempo dos pares de cuentas sin relación entre sí.
Uso de cerrojos de grano grueso
El uso de cerrojos de grano grueso protege una estructura o subsistema grande con un cerrojo, lo que simplifica la corrección pero limita el acceso independiente.
Un mutex de lista hace que cada búsqueda espere detrás de una inserción aunque su objetivo esté muy lejos.

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