← Concurrencia y Sincronización Bloque 1 · El problema de la concurrencia · Tema 1 de 47

Tema 1

Concurrencia y paralelismo

Casi todo programa moderno hace varias cosas «a la vez»: atiende a muchos clientes, mantiene la ventana respondiendo mientras descarga, reparte un cálculo entre todos los núcleos. Pero «a la vez» puede significar dos cosas muy distintas, y confundirlas lleva a decisiones equivocadas, como lanzar ocho hilos esperando que el programa sea ocho veces más rápido. En este tema separarás los dos conceptos con definiciones precisas y con dos experimentos en C, medirás cuánto se puede ganar realmente, verás de dónde sale la concurrencia dentro de un sistema operativo y entenderás por qué, a cambio de sus ventajas, exige coordinación: el tema del resto del curso.

1. Dos palabras que se confunden

En lenguaje corriente «concurrente» y «paralelo» suenan a lo mismo. En sistemas operativos significan cosas diferentes, y la diferencia importa:

Concurrencia es la propiedad de un programa o sistema de tener varias tareas en curso al mismo tiempo: todas han empezado y ninguna ha terminado, aunque en un instante dado solo una esté ejecutándose. Es una cuestión de estructura: cómo se organiza el trabajo en tareas independientes.

Paralelismo es ejecutar varias tareas literalmente en el mismo instante, cada una en su propio procesador o núcleo. Es una cuestión de ejecución: requiere hardware con más de una unidad de cálculo.

De ahí se desprenden tres consecuencias. Se puede tener concurrencia sin paralelismo: un solo núcleo que alterna entre tareas, dándole a cada una un turno corto, como hacía cualquier sistema de tiempo compartido de los años setenta. Se puede tener paralelismo sin concurrencia en sentido interesante: una operación vectorial que suma cuatro pares de números en una sola instrucción. Y lo habitual hoy es tener las dos: muchas tareas concurrentes que el sistema reparte sobre varios núcleos. Este diagrama compara el tiempo de dos tareas, A y B, en los dos escenarios:

Arriba, concurrencia en un solo núcleo: una línea de tiempo con los bloques A, B, A, B, A, B alternados. Abajo, paralelismo en dos núcleos: el núcleo 0 ejecuta A de principio a fin y el núcleo 1 ejecuta B durante el mismo intervalo, por lo que ambas terminan en la mitad del tiempo CONCURRENCIA · un núcleo, las tareas se turnan Núcleo 0 A B A B A B Las dos están en curso desde el principio, pero solo una corre en cada instante: todo dura 6 bloques. PARALELISMO · dos núcleos, las tareas corren a la vez Núcleo 0 Núcleo 1 A B Las dos avanzan al mismo tiempo: el mismo trabajo dura 3 bloques. tiempo
Con un núcleo la concurrencia solo alterna; con dos, el paralelismo ejecuta de verdad a la vez. La concurrencia es la estructura; el paralelismo, una forma de ejecutarla.
Una analogía útil

Un cocinero que prepara dos platos pasando de uno a otro mientras uno hierve y el otro se hornea es concurrente. Dos cocineros, cada uno con su plato, son paralelos. Un buen programa concurrente funciona con uno o con muchos cocineros; que sea más rápido depende de cuántos haya y de en qué se pasa el tiempo.

Vamos a verlo con números. Los dos experimentos siguientes usan hilos de la biblioteca pthreads, que ya conoces de Procesos e Hilos: pthread_create lanza una función en un hilo nuevo y pthread_join espera a que termine.

2. Experimento: repartir un cálculo

El primer programa tiene un trabajo fijo, mil doscientos millones de sumas, y lo reparte en partes iguales entre tantos hilos como se le pida. Los hilos no comparten ningún dato: cada uno acumula en su propia variable local, por lo que no hay nada que coordinar. Al final mide el tiempo transcurrido (el «de pared», como el de un reloj colgado de la pared) y el tiempo de CPU consumido por el proceso entero:

calculo.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define TOTAL 1200000000UL            /* sumas en total, repartidas entre los hilos */

static double segundos(clockid_t reloj)
{
    struct timespec t;
    clock_gettime(reloj, &t);
    return t.tv_sec + t.tv_nsec / 1e9;
}

static void *trabajo(void *arg)
{
    unsigned long vueltas = *(unsigned long *)arg;
    volatile unsigned long suma = 0;          /* variable local: solo este hilo la toca */
    for (unsigned long i = 0; i < vueltas; i++)
        suma += i;
    return NULL;
}

int main(int argc, char *argv[])
{
    int n = argc > 1 ? atoi(argv[1]) : 1;
    if (n < 1 || n > 64)
        return 1;
    pthread_t hilos[64];
    unsigned long vueltas = TOTAL / n;

    double inicio = segundos(CLOCK_MONOTONIC);
    for (int i = 0; i < n; i++)
        if (pthread_create(&hilos[i], NULL, trabajo, &vueltas) != 0) {
            fprintf(stderr, "no se pudo crear el hilo %d\n", i);
            return 1;
        }
    for (int i = 0; i < n; i++)
        pthread_join(hilos[i], NULL);
    double real = segundos(CLOCK_MONOTONIC) - inicio;
    double cpu = segundos(CLOCK_PROCESS_CPUTIME_ID);

    printf("%2d hilo(s): pared %5.2f s   CPU total %5.2f s\n", n, real, cpu);
    return 0;
}

Todos los hilos reciben la misma dirección &vueltas, pero solo la leen: leer un dato compartido que nadie modifica es seguro. Compilamos con -pthread y ejecutamos con 1, 2, 4 y 8 hilos. Primero obligando al proceso a usar un único núcleo con taskset -c 0, y después dejándolo usar los 16 que tiene la máquina de pruebas:

terminal · el mismo trabajo en uno y en todos los núcleos
$ gcc -std=c17 -Wall -Wextra -pthread -o calculo calculo.c
$ nproc
16
$ for n in 1 2 4 8; do taskset -c 0 ./calculo $n; done
 1 hilo(s): pared  2.14 s   CPU total  2.14 s
 2 hilo(s): pared  2.20 s   CPU total  2.20 s
 4 hilo(s): pared  2.06 s   CPU total  2.06 s
 8 hilo(s): pared  2.05 s   CPU total  2.03 s
$ for n in 1 2 4 8; do ./calculo $n; done
 1 hilo(s): pared  2.09 s   CPU total  2.09 s
 2 hilo(s): pared  1.10 s   CPU total  2.18 s
 4 hilo(s): pared  0.55 s   CPU total  2.18 s
 8 hilo(s): pared  0.29 s   CPU total  2.29 s

Los dos bloques cuentan historias opuestas:

Hilos no es lo mismo que velocidad

Un programa limitado por el cálculo solo se acelera con más hilos si hay núcleos libres donde ejecutarlos. Lanzar más hilos que núcleos no ayuda: los extra esperan turno y se suma el costo del cambio de contexto. Un buen punto de partida es un hilo de cálculo por núcleo.

3. Experimento: solapar esperas

Si la concurrencia sin paralelismo no acelera los cálculos, ¿para qué sirve? Para algo muy frecuente: cuando las tareas pasan la mayor parte del tiempo esperando. Una petición a un servidor remoto, una lectura de disco o un clic del usuario dejan a la tarea bloqueada, sin usar CPU. Mientras una espera, otra puede avanzar. Este segundo programa lanza n hilos y cada uno simula una operación de entrada/salida lenta durmiendo medio segundo:

espera.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static double segundos(void)
{
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return t.tv_sec + t.tv_nsec / 1e9;
}

/* Simula una operación de entrada/salida lenta: medio segundo de espera. */
static void *peticion(void *arg)
{
    (void)arg;
    struct timespec medio = { 0, 500000000L };
    nanosleep(&medio, NULL);
    return NULL;
}

int main(int argc, char *argv[])
{
    int n = argc > 1 ? atoi(argv[1]) : 1;
    if (n < 1 || n > 64)
        return 1;
    pthread_t hilos[64];

    double inicio = segundos();
    for (int i = 0; i < n; i++)
        if (pthread_create(&hilos[i], NULL, peticion, NULL) != 0) {
            fprintf(stderr, "no se pudo crear el hilo %d\n", i);
            return 1;
        }
    for (int i = 0; i < n; i++)
        pthread_join(hilos[i], NULL);

    printf("%2d peticiones de 0,5 s: pared %4.2f s\n", n, segundos() - inicio);
    return 0;
}

Lo ejecutamos otra vez con taskset -c 0, es decir, con un único núcleo:

terminal · peticiones concurrentes en un solo núcleo
$ gcc -std=c17 -Wall -Wextra -pthread -o espera espera.c
$ for n in 1 4 10; do taskset -c 0 ./espera $n; done
 1 peticiones de 0,5 s: pared 0.50 s
 4 peticiones de 0,5 s: pared 0.50 s
10 peticiones de 0,5 s: pared 0.50 s

Diez peticiones de medio segundo tardan medio segundo, no cinco. Con un solo núcleo, y sin ningún paralelismo, los tiempos de espera se solapan: los diez hilos se duermen casi al mismo tiempo y el núcleo apenas trabaja. Si el programa las hubiera atendido una tras otra, habría tardado 5 segundos. Esta es la ventaja que da la concurrencia por sí misma, y es el motivo por el que un servidor web atiende miles de conexiones con unos pocos núcleos.

Reunimos las dos lecciones en una regla práctica:

Tipo de tareaDónde pasa el tiempoQué ayudaEjemplo
Limitada por la CPUCalculandoParalelismo: más núcleos, hasta un hilo por núcleo.Comprimir un video, multiplicar matrices.
Limitada por la E/SEsperando disco, red o usuarioConcurrencia: muchas tareas en curso, aunque haya un solo núcleo.Servidor web, cliente de correo, descarga de archivos.

4. Cuánto se puede acelerar: la ley de Amdahl

En calculo.c todo el trabajo se podía repartir. En un programa real casi siempre queda una parte que solo puede hacerse en serie: leer los datos de entrada, combinar los resultados parciales, escribir la salida. Esa parte limita la ganancia sin importar cuántos núcleos se añadan. Gene Amdahl lo formuló en 1967. Si una fracción f del tiempo de ejecución en un núcleo es paralelizable y el resto, 1 − f, es serie, entonces con n núcleos:

Aceleración = 1 / ((1 − f) + f / n). Cuando n crece sin límite, la aceleración tiende a 1 / (1 − f).

Fíjate en el límite: si solo el 90 % del programa es paralelizable, jamás se podrá ir más de 10 veces más rápido, aunque se usen mil núcleos, porque el 10 % serie siempre estará ahí. La tabla muestra la aceleración para tres programas distintos:

Parte paralelizable (f)2 núcleos4 núcleos8 núcleos16 núcleosLímite (n → ∞)
50 %1,331,601,781,882
90 %1,823,084,716,4010
95 %1,903,485,939,1420

Con 16 núcleos, un programa que es 90 % paralelizable acelera 6,4 veces, no 16: usa el 40 % de la potencia disponible. La lección práctica es que, antes de paralelizar, hay que medir cuánto del tiempo corresponde a código que sí se puede repartir. Y la lección técnica, que es la que nos ocupa en este curso, es que la parte «serie» suele ser precisamente la coordinación: los hilos que esperan turno para entrar a una sección protegida con un candado son, de hecho, tiempo serie. Cuanto más se sincroniza, menos se acelera.

5. Probar con un simulador

Este simulador aplica la ley de Amdahl a un trabajo que en un núcleo dura 100 unidades de tiempo. La primera barra muestra ese trabajo con un núcleo, dividido en su parte serie (gris) y su parte paralelizable (azul). La segunda muestra cómo queda con n núcleos: la parte paralelizable se encoge, la serie no, y el espacio vacío es el tiempo que se ahorra. Los valores iniciales, 90 % y 4 núcleos, reproducen una fila de la tabla anterior.

Simulador · la ley de Amdahl

Con 1 núcleo

Con 4 núcleos

MedidaValor
Tiempo con 1 núcleo100
Tiempo con 4 núcleos
Aceleración
Eficiencia (aceleración / núcleos)
Límite con infinitos núcleos

Prueba tres cosas. Pon 100 % y comprueba que la aceleración es igual al número de núcleos: es el caso de calculo.c. Pon 50 % y sube los núcleos a 1024: la aceleración nunca llega a 2. Y con 90 %, compara 8 y 16 núcleos: duplicar el hardware mejora la aceleración solo de 4,7 a 6,4, y la eficiencia cae del 59 % al 40 %.

6. De dónde viene la concurrencia

Los hilos no son la única fuente de concurrencia. Dentro de un sistema operativo hay varias, y todas obligan a pensar en lo mismo: qué ocurre cuando dos actividades tocan un dato a la vez.

FuenteQué es lo concurrente¿Comparten memoria?Ejemplo
ProcesosProgramas independientes alternados por el planificador.No, salvo que lo pidan (memoria compartida, archivos).Un navegador, un editor y un compilador funcionando a la vez.
HilosVarios flujos de ejecución dentro de un mismo proceso.Sí, todo el espacio de direcciones.Un servidor con un hilo por conexión. Es el caso de este curso.
Señales e interrupcionesUn manejador que interrumpe al programa en cualquier punto.Sí: toca las mismas variables que el código interrumpido.Un manejador de SIGINT que modifica un contador global.
E/S asíncrona y bucles de eventosOperaciones pendientes cuyas respuestas llegan en cualquier orden.Sí, pero con un solo hilo hay menos riesgo.Node.js y el JavaScript de un navegador.
El propio núcleoVarios núcleos ejecutando código del sistema a la vez, más las interrupciones del hardware.Sí: estructuras internas compartidas.Dos procesos pidiendo memoria a la vez en distintos núcleos.

El caso que más se estudia, y el que usaremos en los ejemplos, son los hilos de un proceso, porque comparten todo el espacio de direcciones (variables globales, montón) y por eso son los más fáciles de usar y los que más errores producen. Los procesos aislados están protegidos por el sistema de memoria virtual; los hilos no. Los mismos principios, bajo nombres diferentes, sirven para señales, para el núcleo y para sistemas distribuidos.

7. El precio de la concurrencia

Hasta aquí todo fueron ventajas, y las dos que vimos son reales: aprovechar varios núcleos y solapar esperas. El inconveniente es que, en cuanto las tareas concurrentes comparten datos que se pueden modificar, el resultado deja de depender solo del programa. Depende además del orden exacto en que el planificador intercala las instrucciones de cada hilo, y ese orden cambia en cada ejecución, según la carga de la máquina, el número de núcleos o el tamaño de la caché.

Los problemas de la programación concurrente se agrupan en tres familias, y cada una ocupa una parte del curso:

  1. Corrección: las condiciones de carrera. Dos hilos leen y escriben el mismo dato y se pisan: se pierden actualizaciones, se leen valores a medias. El programa no falla con un error claro, simplemente da resultados incorrectos algunas veces. Es el tema de los bloques 1 a 3. datos
  2. Vivacidad: los interbloqueos. Al arreglar lo anterior con candados aparece el problema contrario: dos hilos que esperan cada uno algo que tiene el otro, para siempre. El programa no da resultados incorrectos, sino que deja de avanzar. Es el tema del bloque 8. progreso
  3. Rendimiento: la contención. Cada candado convierte código paralelo en serie, y la ley de Amdahl se aplica sin piedad: un programa con demasiada sincronización puede ser más lento con ocho hilos que con uno. Se trata a lo largo de todo el curso, sobre todo en los bloques 3 y 4. velocidad

Lo difícil de estos errores es que son no deterministas: pueden no aparecer en las pruebas, desaparecer al añadir una línea de depuración y presentarse solo en producción, con carga. No se resuelven con prueba y error, sino razonando sobre todos los intercalados posibles y usando herramientas que los impidan o los detecten. Por eso el curso empieza por entender el problema con precisión (temas 2 a 5) antes de ofrecer las soluciones.

Un adelanto

En el tema 3 escribirás el programa concurrente más sencillo que existe, un contador que varios hilos incrementan, y lo verás dar un resultado distinto en cada ejecución, casi siempre menor que el correcto. Es el «hola mundo» de las condiciones de carrera, y todo lo que sigue consiste en entender por qué ocurre y cómo evitarlo.

8. Ver los hilos en Linux

Antes de estudiar cómo coordinar hilos conviene saber dónde mirarlos. Linux los trata como una especie de proceso ligero, y ofrece varias formas de observarlos. Para probarlas usamos un programa auxiliar que lanza tres hilos que no hacen otra cosa que contar sin parar, y se queda dormido 30 segundos para dar tiempo a inspeccionarlo:

vivos.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

static void *girar(void *arg)
{
    (void)arg;
    volatile unsigned long n = 0;
    for (;;)
        n++;
    return NULL;
}

int main(void)
{
    pthread_t h[3];
    for (int i = 0; i < 3; i++)
        pthread_create(&h[i], NULL, girar, NULL);
    printf("pid %d\n", getpid());
    fflush(stdout);
    sleep(30);
    return 0;
}

Lo ejecutamos en segundo plano y miramos el estado del proceso, sus hilos y el directorio /proc/PID/task, donde el núcleo crea una carpeta por cada hilo:

terminal · los hilos de un proceso
$ gcc -std=c17 -Wall -Wextra -pthread -o vivos vivos.c
$ ./vivos &
pid 752
$ grep Threads /proc/752/status
Threads:	4
$ ps -L -o pid,lwp,nlwp,psr,stat,pcpu,comm -p 752
    PID     LWP NLWP PSR STAT %CPU COMMAND
    752     752    4   5 Sl+   0.0 vivos
    752     754    4   9 Rl+  99.0 vivos
    752     755    4  11 Rl+  99.0 vivos
    752     756    4  13 Rl+  99.0 vivos
$ ls /proc/752/task
752
754
755
756

El proceso tiene 4 hilos: el hilo principal más los tres creados. Cada fila de ps -L es un hilo. PID es el mismo para todos (es el proceso); LWP (lightweight process) es el identificador de cada hilo, y el del hilo principal coincide con el PID; NLWP es el número de hilos del proceso. Las columnas más interesantes son las dos últimas: los tres hilos de cálculo están en estado R (ejecutándose) con un 99 % de CPU cada uno, y PSR indica que cada uno está en un núcleo diferente (9, 11 y 13). Eso es paralelismo real, observado desde fuera. El hilo principal, dormido en sleep, está en estado S y no consume CPU.

Cuántos núcleos tienes

nproc devuelve el número de procesadores lógicos disponibles para el proceso actual; lscpu da más detalle (núcleos físicos, hilos por núcleo, cachés). Dentro de un programa en C, sysconf(_SC_NPROCESSORS_ONLN) devuelve lo mismo. Es el número que sirve de guía para decidir cuántos hilos de cálculo lanzar.

9. Cómo trabajaremos

El curso se apoya en lo que sabes de Procesos e Hilos y en el C de Programación en C para Sistemas Operativos. Combina tres tipos de actividad:

  1. Programas con pthreads en C: cada herramienta de sincronización se prueba con un programa real, primero mostrando el problema y después la solución. Compilan con gcc -std=c17 -Wall -Wextra -pthread. C
  2. Simuladores en JavaScript: como los errores de concurrencia no se reproducen a voluntad, los simuladores permiten recorrer los intercalados paso a paso, forzar el peor caso y ver el estado de cada hilo, el valor de cada semáforo o el ciclo de un interbloqueo. JavaScript
  3. Herramientas de análisis: ThreadSanitizer y Helgrind detectan carreras y mal uso de candados en programas reales; ps -L, top -H y /proc permiten observar los hilos. Linux
terminal · lo necesario, en debian y ubuntu
$ sudo apt install build-essential procps util-linux valgrind
# build-essential trae gcc y make; procps trae ps y top;
# util-linux trae taskset y lscpu; valgrind trae Helgrind
Los números cambiarán

Los tiempos medidos dependen de la máquina: con otro procesador o con otro número de núcleos obtendrás cifras distintas. Lo que debe repetirse es el patrón: sin paralelismo real el tiempo de pared de un cálculo no baja; con él baja casi en proporción mientras haya núcleos libres; y las esperas se solapan aun con un solo núcleo. Y en este curso los resultados de los programas con errores de concurrencia cambiarán en cada ejecución: eso es parte de lo que hay que aprender.

10. El mapa del curso

Los 47 temas van del problema a las soluciones y de las soluciones a sus propios fallos. Cada bloque responde una pregunta:

BloqueTemasLa pregunta que responde
1. El problema de la concurrencia1–5¿Qué falla cuando varios hilos comparten datos?
2. La sección crítica6–10¿Qué código debe ejecutarse de a uno y cómo se garantiza?
3. Operaciones atómicas11–14¿Qué ofrece el hardware y cómo se construyen candados sobre él?
4. Mutex15–19¿Cómo se protege un dato en la práctica, y cómo se detectan los errores?
5. Semáforos20–24¿Cómo se cuentan recursos y se ordena la ejecución entre hilos?
6. Variables de condición y monitores25–28¿Cómo se espera a que algo ocurra sin gastar CPU?
7. Problemas clásicos29–34¿Cómo se resuelven productor-consumidor, lectores-escritores y los filósofos?
8. Interbloqueos35–41¿Cómo se previene, evita y detecta un bloqueo mutuo?
9. Más allá de los candados42–44¿Qué hay por debajo de un mutex y qué alternativas existen?
10. Integración y cierre45–47¿Cómo se combina todo en programas completos?

Los bloques 1 y 2 definen el problema con rigor; el 3 baja al hardware; del 4 al 6 se aprenden las herramientas de la más simple a la más expresiva; el 7 las aplica a los problemas clásicos; el 8 estudia el peor fallo que ellas permiten; y los dos últimos amplían la visión y cierran con proyectos.

11. Resumen

12. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿En qué se diferencian la concurrencia y el paralelismo?

La concurrencia es tener varias tareas en curso a la vez, aunque en cada instante solo avance una; describe cómo está organizado el trabajo. El paralelismo es ejecutar varias tareas en el mismo instante, cada una en su propio núcleo; describe cómo se ejecuta. La concurrencia puede existir con un solo núcleo; el paralelismo requiere varios.

2. En el experimento con taskset -c 0, 8 hilos tardaron lo mismo que 1. ¿Por qué?

Porque el cálculo está limitado por la CPU y solo hay un núcleo. Los ocho hilos están en curso (concurrencia), pero el núcleo solo puede ejecutar uno a la vez, así que el total de trabajo se hace en el mismo tiempo que con un hilo. No hay paralelismo, y por eso no hay ganancia.

3. ¿Por qué el tiempo de CPU total no baja al usar más hilos con varios núcleos?

Porque el trabajo total es el mismo: las mismas 1.200 millones de sumas. El paralelismo lo reparte entre núcleos para que termine antes en el reloj de pared, pero no lo reduce; incluso crece un poco por el costo de crear y alternar hilos.

4. Diez hilos que duermen medio segundo cada uno tardaron medio segundo en total, con un solo núcleo. ¿Qué enseña eso?

Que la concurrencia permite solapar esperas sin necesidad de paralelismo. Mientras los hilos están dormidos no usan CPU, así que todos esperan a la vez; hacerlo en serie habría tomado 5 segundos. Es la razón por la que un servidor atiende muchas conexiones con pocos núcleos.

5. Un programa es 80 % paralelizable. ¿Qué aceleración máxima puede lograr, y cuál con 4 núcleos?

El máximo es 1 / (1 − 0,8) = 5, sin importar cuántos núcleos haya. Con 4 núcleos: 1 / (0,2 + 0,8/4) = 1 / 0,4 = 2,5.

6. ¿Por qué añadir núcleos da rendimientos decrecientes?

Porque la parte serie del programa no se acorta con más núcleos y acaba dominando el tiempo total. Con f = 90 %, pasar de 8 a 16 núcleos solo mejora la aceleración de 4,71 a 6,40, y la eficiencia baja del 59 % al 40 %.

7. ¿Qué tipo de tarea se beneficia más del paralelismo y cuál de la concurrencia en un solo núcleo?

Las tareas limitadas por la CPU, que pasan el tiempo calculando, se benefician del paralelismo: necesitan más núcleos. Las limitadas por la E/S, que pasan el tiempo esperando, se benefician de la concurrencia: mientras una espera, otra avanza.

8. Además de los hilos, ¿qué otras fuentes de concurrencia hay en un sistema?

Los procesos que el planificador alterna, los manejadores de señales e interrupciones que interrumpen al programa en cualquier punto, la entrada/salida asíncrona y los bucles de eventos, y el propio núcleo del sistema operativo, que se ejecuta a la vez en varios núcleos. Todas plantean el problema de qué pasa cuando dos actividades acceden al mismo dato.

9. ¿Cuáles son las tres familias de problemas de la programación concurrente?

Corrección (condiciones de carrera: resultados incorrectos por accesos simultáneos a datos compartidos), vivacidad (interbloqueos y otras situaciones en que el programa deja de avanzar) y rendimiento (contención: la sincronización serializa el código y limita la aceleración).

10. ¿Qué muestran las columnas NLWP y PSR de ps -L?

NLWP es el número de hilos del proceso. PSR es el procesador (núcleo) en que se ejecutó por última vez cada hilo. Si varios hilos en estado R tienen valores distintos de PSR, se están ejecutando en paralelo.

Para practicar

  1. Compila calculo.c y ejecútalo con 1, 2, 4, 8 y 16 hilos, con y sin taskset -c 0. Calcula la aceleración de cada caso y compárala con el número de hilos. ¿Qué ocurre con más hilos que núcleos? Pruébalo con el doble de nproc.
  2. Usa taskset -c 0,1 para limitar el programa a dos núcleos y repite el experimento con 1, 2, 4 y 8 hilos. ¿Cuál es el máximo que logras y por qué?
  3. Modifica espera.c para que cada hilo haga primero 0,1 s de cálculo y después duerma 0,5 s. Ejecútalo con un solo núcleo y con varios, con 1 y con 10 hilos. ¿En qué momento deja de solaparse todo?
  4. Calcula a mano la aceleración con 8 núcleos de un programa 75 % paralelizable. Compruébala con el simulador y averigua con cuántos núcleos se alcanza el 90 % de la aceleración máxima.
  5. Ejecuta vivos y mira con top -H -p PID los hilos del proceso. ¿Qué ves en la columna de CPU? Comprueba con taskset -a -c 0 -p PID (limita todos los hilos a un núcleo) cómo cambia el reparto.
  6. Cuenta con ps -eL --no-headers | wc -l y con ps -e --no-headers | wc -l cuántos hilos y cuántos procesos hay en tu sistema. ¿Qué proceso tiene más hilos? Pista: ps -eo nlwp,comm --sort=-nlwp | head.