Semana 11 — Variables de condición y semáforos

Objetivos

  • Usar variables de condición (condition variables) correctamente: siempre con lock y siempre en un while.
  • Resolver productor/consumidor, lectores/escritores y los filósofos comensales (dining philosophers) con semáforos.

Sesión A — Variables de condición

Lecturas:

Temas:

  • wait y signal; por qué wait libera el lock.
  • Productor/consumidor (bounded buffer) paso a paso: el error del if, el error de una sola CV.
  • Semántica Mesa vs. Hoare. Covering conditions (broadcast).

Sesión B — Semáforos

Lecturas:

Temas:

  • Semáforo como lock (binario) y como variable de condición.
  • Productor/consumidor con semáforos y dónde va el mutex.
  • Lock de lectores/escritores. Filósofos comensales. Throttling.
  • Implementar semáforos con locks y CVs (Zemaphores).
  • Inversión de prioridad: el caso del Mars Pathfinder; herencia y techo de prioridad; PTHREAD_PRIO_INHERIT en Linux.

Lecturas complementarias (inversión de prioridad; OSTEP no la cubre):

Laboratorio — Sincronización

  1. main-*.c del cap. 30: ejecutar cada variante del productor/consumidor y explicar por qué falla o funciona.
  2. Tarea de código del cap. 31: barrera (barrier), rendezvous, lectores/escritores sin inanición, mutex sin inanición.

Entregas

Preguntas de repaso

  1. Muestre una intercalación en la que usar if en lugar de while alrededor de wait rompe el productor/consumidor.
  2. ¿A qué valor se inicializa un semáforo usado como lock? ¿Y uno usado para esperar a que un hilo termine?
  3. ¿Por qué la solución ingenua de los filósofos produce deadlock y cómo se evita?
  4. ¿Qué es la inversión de prioridad? ¿Por qué la herencia de prioridad acota la espera del hilo de prioridad alta?

Código de la semana

Compilar dentro del contenedor del curso con make o gcc -Wall.

Ejemplos (8)Errores (1)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

join.cDescargar join.c
// join.c — el padre espera al hijo con una variable de condición.
//
// Uso: ./join ok        estado + lock + while: funciona siempre
//      ./join nostate   sin la variable done: si el hijo avisa antes,
//                       el aviso se pierde y el padre duerme para siempre
//      ./join nolock    el padre revisa done sin el lock: el hijo avisa
//                       justo entre la revisión y el wait
// Los retrasos (msleep) fuerzan la intercalación mala; un watchdog
// detecta que el padre no despierta.
#include <string.h>
#include "trace.h"

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t c = PTHREAD_COND_INITIALIZER;
static int done = 0;
static const char *mode;

static void thr_exit(void) {
    if (strcmp(mode, "nolock") == 0) {
        done = 1;                       // sin lock
        TRACE("hijo:  done = 1, signal (sin lock)");
        pthread_cond_signal(&c);
    } else {
        pthread_mutex_lock(&m);
        if (strcmp(mode, "ok") == 0)
            done = 1;
        TRACE("hijo:  %ssignal", strcmp(mode, "ok") == 0 ? "done = 1, " : "");
        pthread_cond_signal(&c);
        pthread_mutex_unlock(&m);
    }
}

static void thr_join(void) {
    if (strcmp(mode, "ok") == 0) {
        pthread_mutex_lock(&m);
        while (done == 0) {
            TRACE("padre: done == 0, wait");
            pthread_cond_wait(&c, &m);
        }
        pthread_mutex_unlock(&m);
    } else if (strcmp(mode, "nostate") == 0) {
        pthread_mutex_lock(&m);
        TRACE("padre: wait (no hay estado que revisar)");
        pthread_cond_wait(&c, &m);
        pthread_mutex_unlock(&m);
    } else {                            // nolock
        if (done == 0) {                // revisa sin el lock...
            TRACE("padre: done == 0 (revisado sin lock)");
            msleep(100);                // ...y lo desalojan aquí
            pthread_mutex_lock(&m);
            TRACE("padre: wait");
            pthread_cond_wait(&c, &m);
            pthread_mutex_unlock(&m);
        }
    }
}

static void *child(void *arg) {
    (void)arg;
    if (strcmp(mode, "nolock") == 0)
        msleep(50);                     // llega en medio del padre
    TRACE("hijo:  trabajo terminado");
    thr_exit();
    return NULL;
}

int main(int argc, char *argv[]) {
    mode = argc > 1 ? argv[1] : "ok";
    if (strcmp(mode, "ok") && strcmp(mode, "nostate") && strcmp(mode, "nolock")) {
        fprintf(stderr, "uso: %s ok|nostate|nolock\n", argv[0]);
        return 1;
    }
    trace_start();
    watchdog_start(1, "El padre duerme: el aviso se perdió.", NULL);

    pthread_t p;
    pthread_create(&p, NULL, child, NULL);
    if (strcmp(mode, "nostate") == 0)
        msleep(50);                     // el hijo corre primero
    thr_join();
    TRACE("padre: el hijo terminó");
    watchdog_done();
    pthread_join(p, NULL);
    return 0;
}
pc.cDescargar pc.c
// pc.c — productor/consumidor (bounded buffer) con dos variables de condición.
//
// Uso: ./pc MAX P C N
//   MAX  tamaño del buffer          P  productores
//   C    consumidores               N  elementos por productor
// Cada productor mete N números; los consumidores los sacan hasta recibir
// un -1 (fin). Al final se comprueba que la suma de lo consumido sea la
// de lo producido.
#include "trace.h"

static int *buffer;
static int max;
static int fill_ptr = 0, use_ptr = 0, count = 0;

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t empty = PTHREAD_COND_INITIALIZER; // hay espacio
static pthread_cond_t fill = PTHREAD_COND_INITIALIZER;  // hay datos

static void put(int value) {
    buffer[fill_ptr] = value;
    fill_ptr = (fill_ptr + 1) % max;
    count++;
}

static int get(void) {
    int tmp = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % max;
    count--;
    return tmp;
}

static void produce(int value) {
    pthread_mutex_lock(&mutex);
    while (count == max)                // lleno: esperar espacio
        pthread_cond_wait(&empty, &mutex);
    put(value);
    pthread_cond_signal(&fill);         // despierta a un consumidor
    pthread_mutex_unlock(&mutex);
}

static int consume(void) {
    pthread_mutex_lock(&mutex);
    while (count == 0)                  // vacío: esperar datos
        pthread_cond_wait(&fill, &mutex);
    int tmp = get();
    pthread_cond_signal(&empty);        // despierta a un productor
    pthread_mutex_unlock(&mutex);
    return tmp;
}

static int nitems;

static void *producer(void *arg) {
    long id = (long)arg;
    for (int i = 0; i < nitems; i++)
        produce((int)(id * nitems + i) % 1000);
    return NULL;
}

static void *consumer(void *arg) {
    long *sum = arg;
    for (;;) {
        int v = consume();
        if (v == -1)
            break;
        *sum += v;
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    if (argc != 5) {
        fprintf(stderr, "uso: %s MAX P C N\n", argv[0]);
        return 1;
    }
    max = atoi(argv[1]);
    int np = atoi(argv[2]), nc = atoi(argv[3]);
    nitems = atoi(argv[4]);
    buffer = malloc(sizeof(int) * max);

    pthread_t prod[np], cons[nc];
    long sums[nc];
    for (long i = 0; i < np; i++)
        pthread_create(&prod[i], NULL, producer, (void *)i);
    for (int i = 0; i < nc; i++) {
        sums[i] = 0;
        pthread_create(&cons[i], NULL, consumer, &sums[i]);
    }
    for (int i = 0; i < np; i++)
        pthread_join(prod[i], NULL);
    for (int i = 0; i < nc; i++)
        produce(-1);                    // un fin por consumidor

    long expected = 0, got = 0;
    for (long id = 0; id < np; id++)
        for (int i = 0; i < nitems; i++)
            expected += (id * nitems + i) % 1000;
    for (int i = 0; i < nc; i++) {
        pthread_join(cons[i], NULL);
        printf("consumidor %d: suma %ld\n", i, sums[i]);
        got += sums[i];
    }
    printf("esperado %ld, obtenido %ld: %s\n", expected, got,
           expected == got ? "OK" : "ERROR");
    free(buffer);
    return expected == got ? 0 : 1;
}
philo.cDescargar philo.c
// philo.c — los filósofos comensales con semáforos.
//
// Uso: ./philo naive     todos toman primero el tenedor izquierdo: deadlock
//      ./philo ordered   el filósofo 4 toma primero el derecho: sin ciclo
// Cada filósofo come MEALS veces. Un watchdog detecta el deadlock y
// muestra qué tenedor tiene y cuál espera cada filósofo.
#include <semaphore.h>
#include <string.h>
#include "trace.h"

#define N     5
#define MEALS 1000

static sem_t forks[N];
static int ordered;
static atomic_int holding[N];           // tenedor que tiene (-1: ninguno)
static atomic_int waiting[N];           // tenedor que espera (-1: ninguno)
static int meals[N];

static int left(int p) { return p; }
static int right(int p) { return (p + 1) % N; }

static void take(int p, int f) {
    atomic_store(&waiting[p], f);
    sem_wait(&forks[f]);
    atomic_store(&waiting[p], -1);
}

static void get_forks(int p) {
    int first = left(p), second = right(p);
    if (ordered && p == N - 1) {        // el último rompe el ciclo
        first = right(p);
        second = left(p);
    }
    take(p, first);
    atomic_store(&holding[p], first);
    if (meals[p] == 0)
        msleep(20);                     // la primera vez, todos a la vez
    take(p, second);
}

static void put_forks(int p) {
    sem_post(&forks[left(p)]);
    sem_post(&forks[right(p)]);
    atomic_store(&holding[p], -1);
}

static void *philosopher(void *arg) {
    int p = (int)(long)arg;
    for (int i = 0; i < MEALS; i++) {
        get_forks(p);
        meals[p]++;                     // comer
        progress_bump();
        put_forks(p);
    }
    return NULL;
}

static void report(void) {
    for (int p = 0; p < N; p++)
        printf("  filósofo %d: tiene el tenedor %d, espera el %d\n", p,
               atomic_load(&holding[p]), atomic_load(&waiting[p]));
}

int main(int argc, char *argv[]) {
    const char *mode = argc > 1 ? argv[1] : "";
    if (strcmp(mode, "naive") && strcmp(mode, "ordered")) {
        fprintf(stderr, "uso: %s naive|ordered\n", argv[0]);
        return 1;
    }
    ordered = strcmp(mode, "ordered") == 0;
    for (int i = 0; i < N; i++) {
        sem_init(&forks[i], 0, 1);
        holding[i] = waiting[i] = -1;
    }
    trace_start();
    watchdog_start(1, "Deadlock: espera circular.", report);

    pthread_t t[N];
    for (long i = 0; i < N; i++)
        pthread_create(&t[i], NULL, philosopher, (void *)i);
    for (int i = 0; i < N; i++)
        pthread_join(t[i], NULL);
    watchdog_done();
    TRACE("todos comieron %d veces: sin deadlock", MEALS);
    return 0;
}
prioinv.cDescargar prioinv.c
// prioinv.c — inversión de prioridad real, con pthreads y SCHED_FIFO.
//
// Uso: ./prioinv none      mutex normal: M retrasa a H
//      ./prioinv inherit   PTHREAD_PRIO_INHERIT: L hereda la prioridad de H
// Todos los hilos corren en la CPU 0 con SCHED_FIFO: L (10), M (20), H (30);
// main, con prioridad 40, solo los lanza. L toma el lock y lo usa 50 ms de
// CPU; H llega y pide el lock; M llega y usa 200 ms de CPU sin tocar el lock.
// SCHED_FIFO requiere privilegios (CAP_SYS_NICE): sin ellos, el programa lo
// avisa y termina. env/run.sh lanza el contenedor del curso con ese permiso:
//   env/run.sh sh -c 'cd code/week-11 && make prioinv && ./prioinv none && ./prioinv inherit'
// En un Linux propio: sudo ./prioinv none
#define _GNU_SOURCE
#include <errno.h>
#include <sched.h>
#include <string.h>
#include "trace.h"

static pthread_mutex_t m;
static long h_asked;

// Gasta `ms` milisegundos de CPU de este hilo (no de reloj: si lo desalojan,
// el tiempo que no corre no cuenta).
static void burn(int ms) {
    struct timespec a, b;
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &a);
    do
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &b);
    while ((b.tv_sec - a.tv_sec) * 1000 + (b.tv_nsec - a.tv_nsec) / 1000000 < ms);
}

static void *low(void *arg) {
    (void)arg;
    pthread_mutex_lock(&m);
    TRACE("L (10): toma el lock");
    burn(50);                           // sección crítica larga
    TRACE("L (10): suelta el lock");
    pthread_mutex_unlock(&m);
    return NULL;
}

static void *medium(void *arg) {
    (void)arg;
    TRACE("M (20): empieza (no usa el lock)");
    burn(200);
    TRACE("M (20): termina");
    return NULL;
}

static void *high(void *arg) {
    (void)arg;
    h_asked = now_ms();
    TRACE("H (30): pide el lock");
    pthread_mutex_lock(&m);
    TRACE("H (30): obtiene el lock, esperó %ld ms", now_ms() - h_asked);
    burn(5);
    pthread_mutex_unlock(&m);
    return NULL;
}

// Crea un hilo SCHED_FIFO con la prioridad dada.
static pthread_t spawn(void *(*fn)(void *), int prio) {
    pthread_attr_t a;
    struct sched_param sp = {.sched_priority = prio};
    pthread_attr_init(&a);
    pthread_attr_setinheritsched(&a, PTHREAD_EXPLICIT_SCHED);
    pthread_attr_setschedpolicy(&a, SCHED_FIFO);
    pthread_attr_setschedparam(&a, &sp);
    pthread_t t;
    int e = pthread_create(&t, &a, fn, NULL);
    if (e) {
        fprintf(stderr, "pthread_create: %s\n", strerror(e));
        exit(1);
    }
    pthread_attr_destroy(&a);
    return t;
}

int main(int argc, char *argv[]) {
    const char *mode = argc > 1 ? argv[1] : "";
    int proto;
    if (!strcmp(mode, "none"))
        proto = PTHREAD_PRIO_NONE;
    else if (!strcmp(mode, "inherit"))
        proto = PTHREAD_PRIO_INHERIT;
    else {
        fprintf(stderr, "uso: %s none|inherit\n", argv[0]);
        return 1;
    }

    // Una sola CPU: así M de verdad le quita la CPU a L.
    cpu_set_t one;
    CPU_ZERO(&one);
    CPU_SET(0, &one);
    if (sched_setaffinity(0, sizeof one, &one) != 0) {
        perror("sched_setaffinity");
        return 1;
    }
    struct sched_param sp = {.sched_priority = 40};
    if (sched_setscheduler(0, SCHED_FIFO, &sp) != 0) {
        printf("%s: SCHED_FIFO no permitido (%s).\n", mode, strerror(errno));
        printf("  Hace falta CAP_SYS_NICE: docker run --cap-add=SYS_NICE ... o sudo.\n");
        printf("  Sin privilegios, ./priosim %s muestra lo mismo simulado.\n", mode);
        return 0;
    }

    pthread_mutexattr_t ma;
    pthread_mutexattr_init(&ma);
    pthread_mutexattr_setprotocol(&ma, proto);   // NONE o INHERIT
    pthread_mutex_init(&m, &ma);

    printf("protocolo: %s\n", mode);
    trace_start();
    pthread_t l = spawn(low, 10);
    msleep(10);                         // L ya tiene el lock
    pthread_t h = spawn(high, 30);
    msleep(1);                          // H ya se bloqueó
    pthread_t mm = spawn(medium, 20);
    pthread_join(h, NULL);
    pthread_join(mm, NULL);
    pthread_join(l, NULL);
    return 0;
}
priosim.cDescargar priosim.c
// priosim.c — simulación determinista de la inversión de prioridad.
//
// Uso: ./priosim none      sin protocolo: M retrasa a H sin tocar el lock
//      ./priosim inherit   herencia de prioridad (priority inheritance)
//      ./priosim ceiling   techo de prioridad (priority ceiling)
// Una CPU, prioridades fijas y desalojo: siempre corre el hilo listo de
// mayor prioridad (como SCHED_FIFO). El tiempo avanza en ticks; cada hilo
// sigue un guion de pasos: RUN n (usar la CPU n ticks), LOCK y UNLOCK.
// No necesita privilegios y la salida es siempre la misma.
#include <stdio.h>
#include <string.h>

enum { RUN, LOCK, UNLOCK, END };
struct step { int op, n; };

struct thread {
    const char *name;
    int base, prio;                     // prioridad base y efectiva
    int arrive;                         // tick de llegada
    const struct step *prog;
    int pc, left;                       // paso actual y ticks que le faltan
    int blocked, done, since;           // since: desde cuándo espera la CPU
    char row[64];                       // la línea de tiempo, un carácter por tick
};

// L usa el lock 4 ticks; H lo necesita 2 ticks; M nunca lo toca.
static const struct step progL[] = {{RUN, 1}, {LOCK, 0}, {RUN, 4}, {UNLOCK, 0}, {RUN, 1}, {END, 0}};
static const struct step progM[] = {{RUN, 6}, {END, 0}};
static const struct step progH[] = {{RUN, 1}, {LOCK, 0}, {RUN, 2}, {UNLOCK, 0}, {RUN, 1}, {END, 0}};

#define NT   3
#define CEIL 30                         // techo del lock: la prioridad de H
static struct thread th[NT] = {
    {.name = "H", .base = 30, .prio = 30, .arrive = 2, .prog = progH},
    {.name = "M", .base = 20, .prio = 20, .arrive = 3, .prog = progM},
    {.name = "L", .base = 10, .prio = 10, .arrive = 0, .prog = progL},
};
static struct thread *H = &th[0];

static struct thread *owner;            // owner del lock (NULL: libre)
static int mode;                        // 0 none, 1 inherit, 2 ceiling
static int h_asked;

// Recalcula la prioridad del owner según el protocolo.
static void boost_owner(int t) {
    if (!owner)
        return;
    int p = owner->base;
    if (mode == 2)
        p = CEIL;                       // techo: apenas toma el lock
    if (mode == 1)                      // herencia: la del que más espera
        for (int i = 0; i < NT; i++)
            if (th[i].blocked && th[i].prio > p)
                p = th[i].prio;
    if (p != owner->prio) {
        printf("t=%2d  %s sube a prioridad %d\n", t, owner->name, p);
        owner->prio = p;
    }
}

static void do_unlock(struct thread *me, int t) {
    owner = NULL;
    printf("t=%2d  %s suelta el lock\n", t, me->name);
    if (me->prio != me->base) {
        printf("t=%2d  %s vuelve a prioridad %d\n", t, me->name, me->base);
        me->prio = me->base;
    }
    struct thread *next = NULL;         // el lock pasa al bloqueado de mayor prioridad
    for (int i = 0; i < NT; i++)
        if (th[i].blocked && (!next || th[i].prio > next->prio))
            next = &th[i];
    if (next) {
        next->blocked = 0;
        next->since = t;
        next->pc++;                     // su LOCK ya se cumplió
        owner = next;
        printf("t=%2d  %s obtiene el lock", t, next->name);
        if (next == H)
            printf(" (esperó %d ticks)", t - h_asked);
        printf("\n");
        boost_owner(t);
    }
}

// Ejecuta los pasos instantáneos al frente del guion de `me`, en el instante t.
// Devuelve 1 si quedó listo para usar la CPU; 0 si se bloqueó o terminó.
static int instant_steps(struct thread *me, int t) {
    for (;; me->pc++) {
        const struct step *s = &me->prog[me->pc];
        if (s->op == RUN) {
            if (me->left == 0)
                me->left = s->n;
            return 1;
        }
        if (s->op == END) {
            me->done = 1;
            printf("t=%2d  %s termina\n", t, me->name);
            return 0;
        }
        if (s->op == UNLOCK) {
            do_unlock(me, t);
            continue;
        }
        // LOCK
        if (me == H)
            h_asked = t;
        if (owner) {
            me->blocked = 1;
            printf("t=%2d  %s pide el lock: lo tiene %s, se bloquea\n", t, me->name, owner->name);
            boost_owner(t);
            return 0;
        }
        owner = me;
        printf("t=%2d  %s toma el lock\n", t, me->name);
        boost_owner(t);
    }
}

// ¿Debe correr a antes que b? Mayor prioridad; si empatan, sigue el que
// corría (cur) y si no, el que espera hace más tiempo.
static int before(struct thread *a, struct thread *b, struct thread *cur) {
    if (!b || a->prio != b->prio)
        return !b || a->prio > b->prio;
    if (a == cur || b == cur)
        return a == cur;
    return a->since < b->since;
}

int main(int argc, char *argv[]) {
    const char *m = argc > 1 ? argv[1] : "";
    if (!strcmp(m, "none"))
        mode = 0;
    else if (!strcmp(m, "inherit"))
        mode = 1;
    else if (!strcmp(m, "ceiling"))
        mode = 2;
    else {
        fprintf(stderr, "uso: %s none|inherit|ceiling\n", argv[0]);
        return 1;
    }

    struct thread *cur = NULL;
    int t;
    for (t = 0; t < 60; t++) {
        for (int i = 0; i < NT; i++)
            if (th[i].arrive == t) {
                th[i].since = t;
                printf("t=%2d  %s llega\n", t, th[i].name);
            }
        struct thread *pick;
        do {                            // elegir; si el elegido se bloquea, otra vez
            pick = NULL;
            for (int i = 0; i < NT; i++)
                if (th[i].arrive <= t && !th[i].done && !th[i].blocked &&
                    before(&th[i], pick, cur))
                    pick = &th[i];
        } while (pick && !instant_steps(pick, t));
        if (!pick)
            break;                      // todos terminaron
        if (cur && cur != pick) {
            if (!cur->done && !cur->blocked) {
                printf("t=%2d  %s desaloja a %s\n", t, pick->name, cur->name);
                cur->since = t;
            } else {
                printf("t=%2d  la CPU pasa a %s\n", t, pick->name);
            }
        }
        cur = pick;

        for (int i = 0; i < NT; i++) {  // dibujar el tick t
            struct thread *x = &th[i];
            char c = ' ';
            if (x == pick)
                c = owner == x ? 'X' : '#';
            else if (x->arrive <= t && !x->done)
                c = x->blocked ? '-' : '.';
            x->row[t] = c;
        }
        if (--pick->left == 0) {        // terminó este RUN
            pick->pc++;
            if (pick->prog[pick->pc].op != LOCK)
                instant_steps(pick, t + 1);  // UNLOCK o END: al final del tick
        }
    }

    printf("\n        ");
    for (int i = 0; i < t; i++)
        putchar('0' + i % 10);
    printf("\n");
    for (int i = 0; i < NT; i++)
        printf("%s (%d)  %.*s\n", th[i].name, th[i].base, t, th[i].row);
    printf("X: corre con el lock  #: corre  .: listo  -: bloqueado en el lock\n");
    return 0;
}
sempc.cDescargar sempc.c
// sempc.c — productor/consumidor con semáforos POSIX.
//
// Uso: ./sempc ok        mutex DENTRO de empty/full: funciona
//      ./sempc outside   mutex AFUERA: el consumidor duerme con el mutex
//                        tomado y el productor no puede entrar (deadlock)
// Buffer de MAX elementos, 2 productores y 2 consumidores.
#include <semaphore.h>
#include <string.h>
#include "trace.h"

#define MAX   4
#define ITEMS 50000                     // por productor
#define NP    2
#define NC    2

static int buffer[MAX];
static int fill_ptr = 0, use_ptr = 0;
static sem_t empty, full, mutex;
static int outside;

static void put(int v) { buffer[fill_ptr] = v; fill_ptr = (fill_ptr + 1) % MAX; }
static int get(void) { int v = buffer[use_ptr]; use_ptr = (use_ptr + 1) % MAX; return v; }

static void produce(int v) {
    if (outside) {
        sem_wait(&mutex);               // MAL: el mutex primero
        sem_wait(&empty);
    } else {
        sem_wait(&empty);               // esperar un espacio libre
        sem_wait(&mutex);               // luego, exclusión mutua
    }
    put(v);
    sem_post(&mutex);
    sem_post(&full);                    // un dato más
}

static int consume(const char *who) {
    if (outside) {
        sem_wait(&mutex);
        TRACE("%s: tiene el mutex, sem_wait(&full)", who);
        sem_wait(&full);                // duerme CON el mutex
    } else {
        sem_wait(&full);
        sem_wait(&mutex);
    }
    int v = get();
    sem_post(&mutex);
    sem_post(&empty);
    return v;
}

static void *producer(void *arg) {
    long id = (long)arg;
    if (outside) {
        msleep(50);                     // el consumidor llega primero
        TRACE("P%ld: sem_wait(&mutex)", id);
    }
    for (int i = 0; i < ITEMS; i++) {
        produce(1);
        progress_bump();
    }
    return NULL;
}

static long sums[NC];

static void *consumer(void *arg) {
    long id = (long)arg;
    char who[8];
    snprintf(who, sizeof who, "C%ld", id);
    for (;;) {
        int v = consume(who);
        if (v == -1)
            return NULL;
        sums[id] += v;
        progress_bump();
    }
}

int main(int argc, char *argv[]) {
    const char *mode = argc > 1 ? argv[1] : "";
    if (strcmp(mode, "ok") && strcmp(mode, "outside")) {
        fprintf(stderr, "uso: %s ok|outside\n", argv[0]);
        return 1;
    }
    outside = strcmp(mode, "outside") == 0;
    sem_init(&empty, 0, MAX);           // MAX espacios libres
    sem_init(&full, 0, 0);              // 0 datos
    sem_init(&mutex, 0, 1);             // binario: un lock
    trace_start();
    watchdog_start(1, "Deadlock: C duerme en full con el mutex.", NULL);

    pthread_t p[NP], c[NC];
    for (long i = 0; i < NC; i++)
        pthread_create(&c[i], NULL, consumer, (void *)i);
    for (long i = 0; i < NP; i++)
        pthread_create(&p[i], NULL, producer, (void *)i);
    for (int i = 0; i < NP; i++)
        pthread_join(p[i], NULL);
    for (int i = 0; i < NC; i++)
        produce(-1);                    // un fin por consumidor
    long total = 0;
    for (int i = 0; i < NC; i++) {
        pthread_join(c[i], NULL);
        total += sums[i];
    }
    watchdog_done();
    printf("consumidos %ld de %d: %s\n", total, NP * ITEMS,
           total == NP * ITEMS ? "OK" : "ERROR");
    return 0;
}
throttle.cDescargar throttle.c
// throttle.c — un semáforo hecho con un lock y una CV (un "Zemaphore")
// usado para limitar cuántos hilos entran a la vez a una región costosa.
//
// Uso: ./throttle T K
//   T hilos quieren entrar; el Zemaphore empieza en K.
//   Se mide el máximo de hilos que estuvieron dentro a la vez.
#include "trace.h"

typedef struct {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
} zem_t;

static void zem_init(zem_t *s, int value) {
    s->value = value;
    pthread_cond_init(&s->cond, NULL);
    pthread_mutex_init(&s->lock, NULL);
}

static void zem_wait(zem_t *s) {
    pthread_mutex_lock(&s->lock);
    while (s->value <= 0)               // nunca baja de 0
        pthread_cond_wait(&s->cond, &s->lock);
    s->value--;
    pthread_mutex_unlock(&s->lock);
}

static void zem_post(zem_t *s) {
    pthread_mutex_lock(&s->lock);
    s->value++;
    pthread_cond_signal(&s->cond);
    pthread_mutex_unlock(&s->lock);
}

static zem_t throttle;
static atomic_int inside, max_inside;

static void *worker(void *arg) {
    (void)arg;
    for (int i = 0; i < 5; i++) {
        zem_wait(&throttle);
        int now = atomic_fetch_add(&inside, 1) + 1;
        int old = atomic_load(&max_inside);
        while (now > old && !atomic_compare_exchange_weak(&max_inside, &old, now))
            ;
        msleep(5);                      // región costosa (p. ej., mucha memoria)
        atomic_fetch_sub(&inside, 1);
        zem_post(&throttle);
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "uso: %s T K\n", argv[0]);
        return 1;
    }
    int nt = atoi(argv[1]), k = atoi(argv[2]);
    pthread_t t[nt];
    zem_init(&throttle, k);
    trace_start();
    for (int i = 0; i < nt; i++)
        pthread_create(&t[i], NULL, worker, NULL);
    for (int i = 0; i < nt; i++)
        pthread_join(t[i], NULL);
    printf("%d hilos, límite %d: máximo dentro a la vez = %d (%ld ms)\n",
           nt, k, atomic_load(&max_inside), now_ms());
    return 0;
}
trace.hDescargar trace.h
// trace.h — utilidades para las demostraciones de la semana 11.
//
// TRACE(...)       imprime un evento con el tiempo (ms) desde trace_start().
// progress_bump()  cada hilo avisa que avanzó.
// watchdog_start() un hilo vigila: si nadie avanza en `secs` segundos,
//                  imprime `msg` y termina el programa con código 0
//                  (el error es lo que se quería mostrar).
#ifndef TRACE_H
#define TRACE_H

#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

static struct timespec trace_t0;
static atomic_long progress;
static atomic_int finished;
static int watch_secs;
static const char *watch_msg;
static void (*watch_report)(void);

static inline void trace_start(void) {
    clock_gettime(CLOCK_MONOTONIC, &trace_t0);
}

static inline long now_ms(void) {
    struct timespec t;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return (t.tv_sec - trace_t0.tv_sec) * 1000 +
           (t.tv_nsec - trace_t0.tv_nsec) / 1000000;
}

#define TRACE(...)                                  \
    do {                                            \
        flockfile(stdout);                          \
        printf("[%4ld ms] ", now_ms());             \
        printf(__VA_ARGS__);                        \
        printf("\n");                               \
        fflush(stdout);                             \
        funlockfile(stdout);                        \
    } while (0)

static inline void msleep(int ms) { usleep(ms * 1000); }

static inline void progress_bump(void) { atomic_fetch_add(&progress, 1); }

static inline void *watchdog(void *arg) {
    (void)arg;
    long last = atomic_load(&progress);
    int quiet = 0;                      // décimas de segundo sin avance
    while (!atomic_load(&finished)) {
        msleep(100);
        long p = atomic_load(&progress);
        if (p != last) {
            last = p;
            quiet = 0;
        } else if (++quiet >= watch_secs * 10) {
            if (atomic_load(&finished))
                break;
            TRACE("WATCHDOG: %d s sin avance.", watch_secs);
            printf("          %s\n", watch_msg);
            if (watch_report)
                watch_report();
            exit(0);                    // mata a los hilos dormidos
        }
    }
    return NULL;
}

static inline void watchdog_start(int secs, const char *msg,
                                  void (*report)(void)) {
    pthread_t w;
    watch_secs = secs;
    watch_msg = msg;
    watch_report = report;
    pthread_create(&w, NULL, watchdog, NULL);
    pthread_detach(w);
}

static inline void watchdog_done(void) { atomic_store(&finished, 1); }

#endif

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

pcbug.cDescargar pcbug.c
// pcbug.c — los dos errores del productor/consumidor, forzados paso a paso.
//
// Buffer de UN elemento, un productor (Tp) y dos consumidores (Tc1, Tc2).
//
//   ./pcbug if      if + una CV: Tc2 se cuela y Tc1 lee el buffer vacío
//   ./pcbug while   la misma intercalación con while: Tc1 vuelve a revisar
//   ./pcbug onecv   while + una CV: Tc1 despierta a Tc2 en vez de a Tp
//                   y los tres terminan dormidos
//   ./pcbug twocv   while + dos CVs (empty/fill): siempre funciona
//
// Los msleep() fuerzan el orden de llegada al mutex. Un watchdog detecta
// cuando nadie avanza.
#include <string.h>
#include "trace.h"

static int buffer, count = 0;           // buffer de un elemento
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  // la única CV
static pthread_cond_t empty = PTHREAD_COND_INITIALIZER; // solo en twocv
static pthread_cond_t fill = PTHREAD_COND_INITIALIZER;  // solo en twocv
static int use_if, two_cvs;
static atomic_int bug;

static void put(int v) { buffer = v; count = 1; }

static int get(const char *who) {
    if (count == 0) {                   // en OSTEP: assert(count == 1)
        TRACE("%s: get() con el buffer VACÍO: ¡error!", who);
        bug = 1;
        return -1;
    }
    count = 0;
    return buffer;
}

// Espera mientras (o si) se cumple la condición `cond_expr`.
#define WAIT_WHILE(cond_expr, cv, who, what)                   \
    do {                                                       \
        if (use_if) {                                          \
            if (cond_expr) {                                   \
                TRACE("%s: %s, wait", who, what);              \
                pthread_cond_wait(cv, &mutex);                 \
                TRACE("%s: despierta con el lock", who);       \
            }                                                  \
        } else {                                               \
            while (cond_expr) {                                \
                TRACE("%s: %s, wait", who, what);              \
                pthread_cond_wait(cv, &mutex);                 \
                TRACE("%s: despierta con el lock", who);       \
            }                                                  \
        }                                                      \
    } while (0)

// ---------- Escenario 1 (if / while): Tc2 se cuela ----------
static void *s1_tc1(void *arg) {
    (void)arg;
    pthread_mutex_lock(&mutex);
    WAIT_WHILE(count == 0, &cond, "Tc1", "buffer vacío");
    int v = get("Tc1");
    if (v != -1)
        TRACE("Tc1: consume %d", v);
    pthread_mutex_unlock(&mutex);
    progress_bump();
    return NULL;
}

static void *s1_tc2(void *arg) {
    (void)arg;
    msleep(20);                         // llega mientras Tp tiene el lock
    TRACE("Tc2: pide el lock");
    pthread_mutex_lock(&mutex);
    WAIT_WHILE(count == 0, &cond, "Tc2", "buffer vacío");
    TRACE("Tc2: consume %d (se coló)", get("Tc2"));
    pthread_mutex_unlock(&mutex);
    progress_bump();
    return NULL;
}

static void *s1_tp(void *arg) {
    (void)arg;
    for (int i = 1; i <= 2; i++) {
        msleep(i == 1 ? 10 : 100);
        pthread_mutex_lock(&mutex);
        WAIT_WHILE(count == 1, &cond, "Tp ", "buffer lleno");
        if (i == 1)
            msleep(20);                 // Tc2 se forma en el mutex
        put(i);
        TRACE("Tp : put(%d), signal", i);
        pthread_cond_signal(&cond);
        if (i == 1)
            msleep(20);                 // Tc1 despierta y se forma detrás
        pthread_mutex_unlock(&mutex);
        progress_bump();
    }
    return NULL;
}

// ---------- Escenario 2 (onecv / twocv): todos duermen ----------
// Tp mete 1, 2 y luego un -1 por consumidor. Los consumidores repiten.
static void *s2_consumer(void *arg) {
    const char *who = arg;
    pthread_cond_t *wait_cv = two_cvs ? &fill : &cond;
    pthread_cond_t *sig_cv = two_cvs ? &empty : &cond;
    if (who[2] == '2')
        msleep(5);                      // Tc1 duerme primero, luego Tc2
    for (;;) {
        pthread_mutex_lock(&mutex);
        WAIT_WHILE(count == 0, wait_cv, who, "buffer vacío");
        int v = get(who);
        TRACE("%s: consume %d, signal", who, v);
        pthread_cond_signal(sig_cv);
        pthread_mutex_unlock(&mutex);
        progress_bump();
        if (v == -1)
            return NULL;
    }
}

static void *s2_tp(void *arg) {
    (void)arg;
    int items[] = {1, 2, -1, -1};
    pthread_cond_t *wait_cv = two_cvs ? &empty : &cond;
    pthread_cond_t *sig_cv = two_cvs ? &fill : &cond;
    msleep(20);
    for (int i = 0; i < 4; i++) {
        pthread_mutex_lock(&mutex);
        WAIT_WHILE(count == 1, wait_cv, "Tp ", "buffer lleno");
        put(items[i]);
        TRACE("Tp : put(%d), signal", items[i]);
        pthread_cond_signal(sig_cv);
        pthread_mutex_unlock(&mutex);
        progress_bump();
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    const char *mode = argc > 1 ? argv[1] : "";
    int scenario;
    if (strcmp(mode, "if") == 0) {
        scenario = 1; use_if = 1;
    } else if (strcmp(mode, "while") == 0) {
        scenario = 1;
    } else if (strcmp(mode, "onecv") == 0) {
        scenario = 2;
    } else if (strcmp(mode, "twocv") == 0) {
        scenario = 2; two_cvs = 1;
    } else {
        fprintf(stderr, "uso: %s if|while|onecv|twocv\n", argv[0]);
        return 1;
    }
    trace_start();
    watchdog_start(1, "Tp, Tc1 y Tc2 duermen: nadie los despertará.", NULL);

    pthread_t tp, tc1, tc2;
    if (scenario == 1) {
        pthread_create(&tc1, NULL, s1_tc1, NULL);
        pthread_create(&tc2, NULL, s1_tc2, NULL);
        pthread_create(&tp, NULL, s1_tp, NULL);
    } else {
        pthread_create(&tc1, NULL, s2_consumer, "Tc1");
        pthread_create(&tc2, NULL, s2_consumer, "Tc2");
        pthread_create(&tp, NULL, s2_tp, NULL);
    }
    pthread_join(tc1, NULL);
    pthread_join(tc2, NULL);
    if (scenario == 1 && bug) {         // nadie consumirá el 2: no esperar a Tp
        watchdog_done();
        printf("resultado: ERROR, un consumidor leyó un buffer vacío\n");
        return 0;
    }
    pthread_join(tp, NULL);
    watchdog_done();
    printf("resultado: %s\n", bug ? "ERROR" : "OK");
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 11: variables de condición y semáforos.
#   make         compila todo
#   make test    corre cada demostración (unos 15 s en total)
#                prioinv necesita CAP_SYS_NICE (SCHED_FIFO); sin él, solo avisa
#   make clean   borra los binarios
CFLAGS = -Wall -Wextra -g -pthread
LDLIBS = -pthread
PROGS  = join pc pcbug sempc rwlock philo throttle barrier priosim prioinv
# Las soluciones aún no publicadas (code/.ocultos) no están en el repositorio:
# se omiten los programas sin fuente y sus líneas en las pruebas.
PROGS := $(foreach p,$(PROGS),$(if $(wildcard $(p).c),$(p)))
RUN    = $(if $(wildcard $(1).c),./$(1),@: sin $(1).c)

all: $(PROGS)

$(PROGS): %: %.c trace.h
	$(CC) $(CFLAGS) -o $@ $< $(LDLIBS)

test: $(PROGS)
	./join ok
	./join nostate
	./join nolock
	./pc 4 2 3 100000
	./pcbug if
	./pcbug while
	./pcbug onecv
	./pcbug twocv
	./sempc ok
	./sempc outside
	$(call RUN,rwlock) 1
	$(call RUN,rwlock) 8
	./philo naive
	./philo ordered
	./throttle 8 2
	$(call RUN,barrier) 4 3
	./priosim none
	./priosim inherit
	./priosim ceiling
	./prioinv none
	./prioinv inherit

clean:
	rm -f $(PROGS)

.PHONY: all test clean