Concurrency Preguntas de entrevista
Las preguntas de entrevista sobre Concurrencia evalúan tu capacidad para diseñar y solucionar problemas de sistemas multi-hilo. A los ingenieros senior y equipos de plataforma a menudo se les examina sobre seguridad en hilos, estrategias de bloqueo e implicaciones de rendimiento. Domina estas preguntas para demostrar un pensamiento profundo a nivel de sistema.
Lo que cubren las entrevistas de Concurrency
Seguridad en Hilos y Atomicidad
Asegurar que el estado compartido se acceda correctamente bajo acceso concurrente, usando locks, operaciones atómicas o datos inmutables.
Deadlocks y LiveLocks
Reconocer y prevenir condiciones donde los hilos se bloquean indefinidamente, incluyendo orden de locks y mecanismos de timeout.
Modelo de Memoria y Visibilidad
Comprensión de las relaciones happens-before, volatile y cómo los cachés de CPU afectan la corrección multi-hilo.
Patrones de Concurrencia
Implementaciones de productor-consumidor, lock de lector-escritor, pool de hilos y modelo actor.
Ejemplos de preguntas de entrevista sobre Concurrency
- Explica la diferencia entre proceso e hilo. Da un ejemplo donde threading sea mejor que multiprocessing.Lo que cubre una buena respuesta
- Un proceso tiene espacio de direcciones propio; un hilo comparte el espacio con otros hilos del mismo proceso.
- Crear hilos es más ligero que crear procesos (menos overhead).
- La comunicación entre hilos es más rápida (memoria compartida) que entre procesos (IPC).
- Threading es mejor para tareas I/O-bound o que requieren mucha interacción; multiprocessing para CPU-bound con aislamiento.
Ver respuesta de ejemplo
Un proceso es una instancia en ejecución de un programa, con su propio espacio de direcciones, recursos y estado. Un hilo es la unidad mínima de ejecución dentro de un proceso; todos los hilos de un mismo proceso comparten memoria y recursos. La principal diferencia es que los procesos están aislados entre sí, mientras que los hilos cooperan mediante memoria compartida. Threading es mejor que multiprocessing cuando la tarea tiene muchas operaciones de I/O (como leer archivos o esperar respuestas de red) porque los hilos pueden solaparse sin el overhead de crear nuevos procesos. Por ejemplo, un servidor web que maneja múltiples conexiones simultáneas se beneficia de threading: cada conexión se atiende en un hilo ligero que comparte datos, evitando la copia de memoria y la comunicación entre procesos. En cambio, para tareas intensivas en CPU como procesamiento de imágenes, multiprocessing puede ser superior si el sistema tiene varios núcleos, ya que evita el GIL en Python o problemas de contención en memoria compartida.
- ¿Qué es una condición de carrera? Muestra un fragmento de código simple en Java/C++ que contenga una y corrígelo usando un lock.Lo que cubre una buena respuesta
- Condición de carrera: ocurre cuando el resultado de una operación depende del orden temporal de ejecución de hilos.
- Ejemplo clásico: incremento de un contador compartido sin sincronización.
- Solución: usar un mutex (lock) para proteger la sección crítica.
Ver respuesta de ejemplo
Una condición de carrera (race condition) es un comportamiento inesperado debido a que múltiples hilos acceden a datos compartidos sin sincronización, y el resultado final depende del orden de ejecución. Por ejemplo, en el siguiente código Java, dos hilos incrementan un contador compartido; la operación 'contador++' no es atómica (lectura, incremento, escritura), por lo que un hilo puede leer un valor desactualizado, provocando pérdida de actualizaciones. Para corregirlo, se usa un lock (synchronized o ReentrantLock) que garantiza exclusión mutua alrededor de la sección crítica.
Solución de referenciajava // Código con condición de carrera public class Contador { private int contador = 0; public void incrementar() { contador++; // Lectura, incremento, escritura no atómicos } public int getContador() { return contador; } } // Corrección usando synchronized public class ContadorSeguro { private int contador = 0; public synchronized void incrementar() { contador++; } public synchronized int getContador() { return contador; } } - Implementa una cola limitada thread-safe (cola bloqueante) en C++ o Java con variables de condición.Lo que cubre una buena respuesta
- Cola bloqueante: permite push/pop con límite de capacidad; bloquea si llena o vacía.
- Usar variables de condición (pthread_cond_t en C++, Condition en Java) para esperar/notificar.
- Implementar con un array circular y mutex.
Ver respuesta de ejemplo
Una cola limitada thread-safe (bounded blocking queue) soporta operaciones de inserción y extracción que bloquean al hilo si la cola está llena o vacía respectivamente. Se implementa con un mutex para proteger el estado compartido y dos variables de condición: 'no llena' y 'no vacía'. Cuando un hilo intenta poner un elemento y la cola está llena, espera en 'no llena'; cuando se extrae un elemento, se notifica a los productores. Similarmente, si la cola está vacía, los consumidores esperan en 'no vacía'. El siguiente código en Java implementa una cola bloqueante con un array circular.
Solución de referenciajava import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class BoundedBlockingQueue<T> { private final T[] buffer; private int head, tail, count; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); @SuppressWarnings("unchecked") public BoundedBlockingQueue(int capacity) { buffer = (T[]) new Object[capacity]; head = 0; tail = 0; count = 0; } public void put(T item) throws InterruptedException { lock.lock(); try { while (count == buffer.length) { notFull.await(); // espera hasta que haya espacio } buffer[tail] = item; tail = (tail + 1) % buffer.length; count++; notEmpty.signal(); // notifica a consumidores } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); // espera hasta que haya elementos } T item = buffer[head]; head = (head + 1) % buffer.length; count--; notFull.signal(); // notifica a productores return item; } finally { lock.unlock(); } } } - ¿Cómo detectarías un deadlock programáticamente? Escribe pseudocódigo para un algoritmo de detección de deadlocks.Lo que cubre una buena respuesta
- Deadlock: dos o más hilos esperan recursos retenidos por otros en un ciclo.
- Detección: usar grafo de espera (wait-for graph) y buscar ciclos.
- Alternativas: timeout, lock hierarchy, o utilizar herramientas como jstack.
Ver respuesta de ejemplo
Para detectar deadlocks programáticamente, se puede mantener un grafo de espera (wait-for graph) donde los nodos son hilos y recursos, y las aristas indican qué hilo espera qué recurso. Un deadlock existe si hay un ciclo en el grafo. El pseudocódigo siguiente describe un algoritmo que, cada cierto tiempo, construye el grafo a partir de la información de bloqueo y ejecuta DFS para detectar ciclos. Si se encuentra un ciclo, se puede romper el deadlock (por ejemplo, terminando un hilo). En sistemas reales, se usan herramientas como jstack (Java) o gdb (C++) para volcar los estados de los hilos.
Solución de referenciapseudocode // Algoritmo de detección de deadlocks usando wait-for graph function detectDeadlock(threads, locks): graph = empty directed graph for each thread t in threads: if t is waiting for lock l held by thread s: add edge t -> s in graph // Buscar ciclo con DFS visited = set() recStack = set() for each node in graph: if not visited[node]: if hasCycle(node, visited, recStack, graph): return true // deadlock detectado return false function hasCycle(node, visited, recStack, graph): visited.add(node) recStack.add(node) for each neighbor in graph[node]: if neighbor not in visited: if hasCycle(neighbor, visited, recStack, graph): return true else if neighbor in recStack: return true recStack.remove(node) return false - ¿Qué es el problema ABA en programación lock-free? Proporciona un escenario concreto y su solución.Lo que cubre una buena respuesta
- Problema ABA: valor de una variable cambia de A a B y luego de vuelta a A; operaciones CAS no lo detectan.
- Escenario: lista enlazada lock-free con punteros; reutilización de nodos.
- Solución: usar contadores de versión (etiquetas) para evitar confusiones.
Ver respuesta de ejemplo
El problema ABA ocurre en algoritmos lock-free que usan operaciones de comparación e intercambio (CAS). Si un hilo lee un valor A, luego otro hilo cambia el valor a B y luego de vuelta a A, el CAS del primer hilo tiene éxito aunque el estado intermedio haya cambiado. Un ejemplo concreto es una pila lock-free donde los nodos se reutilizan: un hilo lee el tope (nodo X), otro hilo extrae X y lo reutiliza, y luego el tope vuelve a ser X; el CAS falla al comparar direcciones pero no detecta la modificación. La solución común es usar un contador de versión o timestamp junto con la dirección (ej. doble palabra etiquetada). Así, aunque la dirección coincida, la versión cambia y el CAS falla. En Java, AtomicStampedReference proporciona esta funcionalidad.
- Diseña un rastreador web concurrente que respete robots.txt y use un pool de hilos. Muestra los componentes principales.Lo que cubre una buena respuesta
- Rastreador web: descargar páginas, extraer enlaces, respetar robots.txt.
- Componentes: scheduler de URLs, pool de hilos, parser, gestor de robots.txt.
- Respetar robots.txt: cachear y verificar políticas por dominio antes de descargar.
Ver respuesta de ejemplo
Un rastreador web concurrente consta de varios componentes principales: un scheduler de URLs que mantiene una cola de URLs por visitar y evita repeticiones; un pool de hilos que ejecuta las tareas de descarga; un descargador HTTP que obtiene el contenido; un parser que extrae enlaces; y un gestor de robots.txt que almacena y consulta las políticas de cada dominio. El pool de hilos permite descargas concurrentes controladas. Antes de descargar una URL, el hilo consulta el gestor de robots.txt para verificar si está permitido; si no, la URL se descarta. Se debe implementar cortesía (delay entre solicitudes al mismo dominio) para no sobrecargar servidores. El scheduler puede usar una cola prioritaria (BFS) y un conjunto de URLs visitadas para evitar duplicados.
- Explica el modelo de memoria de Java: ¿qué garantías proporciona 'volatile'? Usa un ejemplo de una bandera compartida.Lo que cubre una buena respuesta
- Volatile garantiza visibilidad y orden (happens-before), pero no atomicidad.
- Ejemplo: bandera booleana compartida para detener un hilo.
- Sin volatile, el hilo puede cachear el valor y nunca ver la actualización.
Ver respuesta de ejemplo
En el modelo de memoria de Java, 'volatile' garantiza que cualquier escritura a una variable volátil es visible inmediatamente para otros hilos (no hay reordenamiento con lecturas/escrituras previas). Además, establece una relación happens-before: leer una variable volátil después de escribirla ve el nuevo valor. Sin embargo, no proporciona atomicidad para operaciones compuestas (ej. contador++). Un ejemplo clásico es una bandera booleana compartida usada para detener un hilo: un hilo escribe 'running = false' y otro hilo lee 'running' en el bucle; si la variable no es volátil, el hilo lector podría quedarse en un bucle infinito porque el cambio no se propaga. Declarar la bandera como volatile resuelve el problema de visibilidad.
Solución de referenciajava public class HiloDetenible extends Thread { private volatile boolean running = true; public void run() { while (running) { // trabajo } } public void detener() { running = false; } } - Si tienes un contador de alta contención, ¿usarías un mutex, atomic o un enfoque lock-free? Compara rendimiento y trade-offs.Lo que cubre una buena respuesta
- Alta contención: muchos hilos compiten por el mismo recurso.
- Mutex: simple pero causa bloqueo y cambio de contexto; atómico: mejor rendimiento para contadores simples; lock-free: más complejo pero evita bloqueos.
- Trade-offs: atómico (como AtomicInteger) es la opción intermedia; mutex es más caro; CAS lock-free puede sufrir contención en caché.
Ver respuesta de ejemplo
Para un contador de alta contención, la elección depende del nivel de concurrencia y la latencia aceptable. Un mutex (como synchronized) es la opción más simple, pero con alta contención provoca bloqueo de hilos, cambio de contexto y posible inversión de prioridad; el rendimiento se degrada rápidamente. Un tipo atómico (como AtomicInteger en Java o std::atomic<int> en C++) usa CAS sin bloqueo, ofreciendo mejor rendimiento que un mutex para operaciones simples como incremento, pero puede sufrir contención en el bus de caché (muchos fallos CAS) si demasiados hilos compiten. Un enfoque lock-free más elaborado (por ejemplo, contador distribuido o eliminación de colas) puede escalar mejor pero es complejo de implementar. En la práctica, para un contador simple, AtomicInteger suele ser la mejor opción por su equilibrio entre rendimiento y simplicidad. Si la contención es extrema y se requiere mínima latencia, se pueden usar técnicas como hilo-local counters con actualización perezosa.
Cómo prepararse
- Practica implementar primitivas clásicas de concurrencia (mutex, semáforo, variable de condición) desde cero.
- Estudia errores del mundo real: deadlock, livelock e inversión de prioridad. Sabe cómo diagnosticar con volcados de hilos.
- Comprende la diferencia entre concurrencia y paralelismo, y cuándo usar cada uno.
- Aprende técnicas lock-free: CAS, memoria transaccional y hazard pointers.
- Prepárate para discutir métricas de rendimiento (throughput vs latencia) bajo contención.
Preguntas frecuentes
¿Necesito conocer cada API de concurrencia en Java/C++?
Enfócate en patrones centrales: locks, semáforos, pools de hilos y operaciones atómicas. Profundidad sobre amplitud.
¿Cómo me preparo para una pregunta de diseño de concurrencia?
Practica diseñando sistemas como pools de hilos, limitadores de tasa y tablas hash concurrentes. Esboza los trade-offs.
¿Cuál es el concepto de concurrencia más complicado?
Particularidades del modelo de memoria, como el reordenamiento y la visibilidad sin sincronización adecuada.
¿Me pedirán que escriba código en una pizarra?
Sí, espera implementar estructuras de datos thread-safe o corregir errores concurrentes manualmente.
¿Qué tan importante es la programación lock-free?
Es avanzado pero muestra comprensión profunda. Conoce CAS básico y la solución ABA.
Practica preguntas sobre Concurrency con retroalimentación instantánea de IA
Sube tu currículum, obtén una entrevista simulada personalizada y ve exactamente qué mejorar — gratis para empezar.