¿Qué es CS Interactive Workshop?
Un taller en el navegador donde los temas difíciles de computación se ven, se oyen y se tocan. En vez de leer sobre un deadlock, lo provocas. En vez de memorizar Big-O, ves la diferencia.
Un taller de computación para aprender haciendo
Hecho para estudiantes de computación · ECCI · UCR
Creado por Josué Torres Sibaja
Todo lo que necesitas saber antes de entrar, en un minuto.
Un taller en el navegador donde los temas difíciles de computación se ven, se oyen y se tocan. En vez de leer sobre un deadlock, lo provocas. En vez de memorizar Big-O, ves la diferencia.
Para estudiantes de Ciencias de la Computación, en especial de la ECCI de la UCR, que están llevando algoritmos, programación concurrente, redes o bases de datos, o que se preparan para entrevistas técnicas. También para cualquier curioso.
Escoges una mesa de trabajo (un curso o tema) y luego una estación. Cada estación tiene tres pasos:
No. Todo corre en tu navegador. Si quieres ir más lejos, el repositorio incluye los motores originales en C y C++ para compilarlos en tu máquina.
Lo creó Josué Torres Sibaja, estudiante de la ECCI, a partir de proyectos reales de sus cursos. Es código abierto (MIT): puedes usarlo, mejorarlo y proponer estaciones nuevas.
Ver el código en GitHubEscoge tu mesa de trabajo.
Ordenamiento, carreras · Pilas, listas y árboles
4 estacionesHilos, semáforos, deadlocks y paralelismo
2 estacionesSockets TCP, HTTP y servidores
1 estaciónFáciles, medios y difíciles: palíndromos, islas, ventanas y más
3 estacionesSQL ejecutado cláusula por cláusula
1 estaciónAmdahl, Big-O y chuletas rápidas
AbrirEl Taller /
Más estaciones en construcción.
Tu progreso en el taller.
Así crece el trabajo cuando el arreglo crece.
std::sort usa Introsort: Quick + Heap + Insertion.std::stable_sort se basa en Merge Sort.Los dos algoritmos ordenan exactamente el mismo arreglo. En cada tic, cada uno hace una sola operación: comparar dos valores o escribir uno. Gana el que necesita menos operaciones para terminar.
Así crece el trabajo cuando el arreglo crece.
| Algoritmo | Mejor | Promedio | Peor |
|---|---|---|---|
| Quick Sort | n log n | n log n | n² |
| Merge Sort | n log n | n log n | n log n |
| Heap Sort | n log n | n log n | n log n |
| Shell Sort | n log n | n^(4/3) | n² |
| Insertion Sort | n | n² | n² |
| Selection Sort | n² | n² | n² |
| Bubble Sort | n | n² | n² |
| Radix Sort | n·k | n·k | n·k |
#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
long long comparaciones = 0;
bool menor(int a, int b) { ++comparaciones; return a < b; } // cuenta cada comparación
int main() {
std::mt19937 rng(42); // misma semilla: el mismo arreglo para todos
std::vector<int> base(100000);
for (int& x : base) x = rng() % 1000;
auto correr = [&](const char* nombre, auto ordenar) {
std::vector<int> a = base; // cada retador ordena su propia copia
comparaciones = 0;
auto t0 = std::chrono::steady_clock::now();
ordenar(a);
auto t1 = std::chrono::steady_clock::now();
std::cout << nombre << ": " << comparaciones << " comparaciones, "
<< std::chrono::duration<double, std::milli>(t1 - t0).count() << " ms\n";
};
correr("std::sort", [](auto& a) { std::sort(a.begin(), a.end(), menor); });
correr("std::stable_sort", [](auto& a) { std::stable_sort(a.begin(), a.end(), menor); });
}
-O2 y corre varias veces: la primera vuelta suele ser más lenta.Apaga los semáforos y mira qué pasa cuando cuatro autos se esperan en círculo.
Cada auto es un hilo y cada cuadrante es un recurso. Un deadlock aparece solo si se cumplen las cuatro condiciones de Coffman al mismo tiempo:
La llave de un baño: una persona a la vez, y quien la toma la devuelve.
Protege secciones críticas cortas. Tiene dueño: solo el hilo que hizo lock puede hacer unlock.
Un parqueo con N espacios: si hay campo entras, si no esperas.
Un contador sin dueño. Sirve para limitar capacidad y para que un hilo le avise a otro.
El guía del tour: nadie sigue hasta que llegue todo el grupo.
Sincroniza fases. La simulación de calor la usa para que ningún hilo empiece la iteración siguiente antes de tiempo.
Un torniquete: avanza de un clic, sin candados.
Operaciones que el procesador hace de una sola vez. Ideales para contadores y banderas compartidas.
// Compila con: gcc -std=c11 -pthread rotonda.c #include <pthread.h> #include <semaphore.h> #define QUADRANTS 4 #define CAPACIDAD 2 // autos por cuadrante int recorrido = 2; // cuadrantes que avanza cada auto sem_t entrada; // cupo total de la rotonda sem_t cuadrante[QUADRANTS]; // capacidad de cada cuadrante void* auto_hilo(void* arg) { int q = *(int*)arg; // cuadrante de entrada sem_wait(&entrada); // nunca se llena del todo sem_wait(&cuadrante[q]); for (int i = 0; i < recorrido; ++i) { int sig = (q + 1) % QUADRANTS; sem_wait(&cuadrante[sig]); // pide el siguiente… sem_post(&cuadrante[q]); // …y suelta el actual q = sig; } sem_post(&cuadrante[q]); sem_post(&entrada); return NULL; } int main(void) { sem_init(&entrada, 0, QUADRANTS * CAPACIDAD - 1); // la clave: -1 for (int i = 0; i < QUADRANTS; ++i) sem_init(&cuadrante[i], 0, CAPACIDAD); /* crear hilos con pthread_create, esperarlos con pthread_join */ }
printf ni E/S con un mutex tomado.sem_init y pthread_mutex_init lleva su destroy.while(!listo);): bloquea con un semáforo o una variable de condición.-fsanitize=thread para detectar condiciones de carrera.valgrind --tool=helgrind ./prog encuentra errores de sincronización.gdb y mira dónde está cada hilo con thread apply all bt.1 a 3: mira cada fila. 4 en adelante: muchas iteraciones por cuadro.
Estable: r ≤ 1/4.
Haz clic o arrastra sobre la placa para dibujar una fuente fija.
Cada columna es una iteración. Nadie cruza la línea punteada (la barrera) hasta que termina el último.
El cambio más grande de cada iteración, en escala logarítmica. Cuando baja de ε, la placa llegó al equilibrio.
El calor fluye de lo caliente a lo frío. Si dividimos la placa en una cuadrícula, cada punto se acerca un poquito al promedio de sus cuatro vecinos en cada paso. Repetir eso muchas veces es resolver la ecuación del calor.
nuevo[i][j] = viejo[i][j] + r * ( viejo[i-1][j] + viejo[i+1][j]
+ viejo[i][j-1] + viejo[i][j+1]
- 4 * viejo[i][j] )
Copiar la tarea del compañero mientras él la sigue corrigiendo sale mal.
Se lee siempre de la placa vieja y se escribe en la nueva (doble búfer). Así ningún hilo lee un valor a medio actualizar, y el resultado es idéntico con 1 o con 8 hilos, sin importar en qué orden terminen.
Cada hilo recibe una franja de filas contiguas. Para calcular el borde de su franja necesita leer una fila del vecino (las "filas fantasma"). No hay problema: esa fila está en la placa vieja, que nadie modifica durante la iteración.
El guía del tour: nadie avanza hasta que llegue todo el grupo.
Antes de intercambiar las placas, todos los hilos deben terminar. La barrera hace esperar a los rápidos, así que el más lento marca el ritmo. Activa "El hilo 0 va más lento" o el reparto desbalanceado y mira cuánto tiempo se pierde esperando.
Cada hilo guarda el cambio más grande de su franja. Después de la barrera se toma el máximo de todos (una reducción) y, si es menor que ε, la placa llegó al equilibrio.
r mide qué tan grande es cada paso en el tiempo. Si r pasa de 1/4, cada iteración amplifica el error en vez de suavizarlo y aparecen ondas que crecen sin control. Sube r a 0.30 y míralo explotar.
Medimos los motores en C de este proyecto con 4 hilos (resultado idéntico al serial en todos los casos):
| Placa | pthreads, barrera que gira | pthreads, barrera que duerme | OpenMP |
|---|---|---|---|
| 120 × 120 | 1.73x | 0.19x | 2.52x |
| 400 × 400 | 3.36x | 1.40x | 3.50x |
| 1000 × 1000 | 1.42x | 1.60x | 1.37x |
Dos lecciones. En placas chicas cada iteración dura microsegundos, y una barrera que duerme al hilo en el sistema operativo cuesta más que el trabajo: con ella, 4 hilos son 5 veces más lentos que 1. En placas enormes los datos ya no caben en la caché y todos quedan esperando a la memoria: más hilos casi no ayudan.
// Cada hilo: su franja, sus propios punteros, una barrera por iteración static void* heat_worker(void* arg) { ... heat_partition(s->rows, s->threads, tid, &begin, &end); double* cur = s->grid_a; // copia local de los punteros double* next = s->grid_b; for (long it = 0; it < s->max_iterations; ++it) { int parity = it & 1; // dos juegos de casillas s->deltas[parity * P + tid].value = heat_step_rows(cur, next, cols, r, begin, end); spin_barrier_wait(&s->barrier, &sense); double global = 0.0; // todos calculan el mismo máximo for (int t = 0; t < P; ++t) global = fmax(global, s->deltas[parity * P + t].value); double* tmp = cur; cur = next; next = tmp; if (global <= s->epsilon) break; // misma decisión en todos los hilos } }
void spin_barrier_wait(spin_barrier_t* b, int* local) { int mine = !*local; *local = mine; if (atomic_fetch_sub(&b->remaining, 1) == 1) { atomic_store(&b->remaining, b->total); atomic_store(&b->sense, mine); // abre } else { while (atomic_load(&b->sense) != mine) if (++spins > 2000) sched_yield(); } }
#pragma omp parallel while (!done) { #pragma omp for schedule(static) reduction(max:step) for (int i = 1; i < rows - 1; ++i) step = fmax(step, heat_step_rows(cur, next, cols, r, i, i + 1)); #pragma omp single { swap(&cur, &next); done = step <= eps; step = 0; } }
CLOCK_MONOTONIC.-fsanitize=thread.cd core-c-engines/concurrency/simulations/heat-diffusion make -C pthreads && make -C openmp ./pthreads/bin/heat_pthreads 400 400 4 0.05 --compare ./pthreads/bin/heat_pthreads 400 400 4 0.05 --compare --sleep-barrier ./openmp/bin/heat_openmp 400 400 4 0.05 --compare
--compare corre también la versión serial e imprime la aceleración y si las placas son idénticas.
common/heat.c), y las pruebas usan esa misma biblioteca en vez de una copia.pthread_create fallido no deje hilos atrapados en la barrera.Dos programas en dos computadoras: el cliente pide una figura y el servidor la busca en su disco y la devuelve. Por debajo hay dos protocolos trabajando juntos: TCP se encarga de que los bytes lleguen completos y en orden, y HTTP define qué dicen esos bytes.
El cliente solo hace socket() → connect() → Write → Read → Close().
"¿Me oyes?" "Sí, ¿y tú a mí?" "Sí." Y ya pueden hablar.
SYN, SYN-ACK y ACK sincronizan los números de secuencia de ambos lados. Desde ahí, cada byte enviado tiene un número, y el ack dice "ya recibí todo hasta aquí".
Un mensaje HTTP es texto plano: una primera línea (método y ruta, o código de estado), encabezados Nombre: valor, una línea vacía y después el cuerpo. Cada línea termina en \r\n.
Tu servidor no envía Content-Length en las figuras: el cliente sabe que terminó cuando el servidor cierra la conexión (Connection: close).
Cada cliente que entra recibe su propio mesero.
El hilo principal solo hace accept(). Por cada conexión crea un std::thread(task, client) que lee, responde y cierra. Así un cliente lento no bloquea a los demás. Con miles de clientes conviene un pool fijo de hilos.
TCP no manda mensajes, manda un flujo de bytes partido en segmentos. En Ethernet con IPv6 cada segmento lleva como máximo 1440 bytes de datos (el MSS). Pide charizard y verás la respuesta llegar en varios pedazos.
Tu servidor llama a Close() apenas termina de escribir, así que él envía el primer FIN. Por eso es el servidor el que queda en TIME_WAIT: un detalle que importa cuando reinicias el servidor y el puerto parece ocupado.
#include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <cstring> int main() { int server = socket(AF_INET, SOCK_STREAM, 0); int yes = 1; setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_port = htons(1234); // ¡orden de red! addr.sin_addr.s_addr = INADDR_ANY; bind(server, (sockaddr*)&addr, sizeof addr); listen(server, 16); while (true) { int client = accept(server, nullptr, nullptr); char buf[4096]; ssize_t n = recv(client, buf, sizeof buf, 0); // puede llegar incompleto const char* res = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhola\n"; send(client, res, strlen(res), 0); close(client); // tu servidor: std::thread(task, client) } }
recv puede devolver menos bytes de los que esperas. Lee en ciclo hasta tener el mensaje completo.htons/ntohs para puertos: la red es big-endian.SO_REUSEADDR evita el error «Address already in use» al reiniciar el servidor.-1 ignorado es un bug escondido.curl -v http://localhost:1234/GET/batman muestra la petición y la respuesta completas.nc localhost 1234 te deja escribir HTTP a mano.tcp.port == 1234 te enseña cada paquete real.Anota lo que quieras recordar de cada tema. Tus notas se guardan solas en este dispositivo.
¿Cuánto más rápido va tu programa si lo paralelizas? Depende de la parte que no se puede paralelizar.
// Mutex pthread_mutex_lock(&m); /* ... */ pthread_mutex_unlock(&m); // Semáforo sem_wait(&s); /* ... */ sem_post(&s); // Barrera pthread_barrier_wait(&b); // Atómico atomic_fetch_add(&contador, 1);
El servidor hace las seis llamadas en ese orden; el cliente solo necesita socket, connect, send/recv y close.
| Algoritmo | Mejor | Promedio | Peor | Memoria | Estable | Úsalo cuando… |
|---|---|---|---|---|---|---|
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No | Necesitas velocidad en memoria en el caso general. |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Sí | Necesitas estabilidad o garantía en el peor caso. |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No | La memoria es muy limitada. |
| Shell Sort | O(n log n) | O(n^(4/3)) | O(n²) | O(1) | No | Arreglos medianos, sin recursión. |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Sí | Datos casi ordenados o muy pocos (n ≤ 16). |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No | Escribir es muy caro (hace pocas escrituras). |
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Sí | Para aprender; casi nunca en producción. |
| Radix Sort | O(n·k) | O(n·k) | O(n·k) | O(n + k) | Sí | Enteros o claves de largo fijo. |