Reparte lecturas de sensores entre hilos en Python, calcula resúmenes parciales y únelos sin condiciones de carrera ni resultados mal ordenados.
Código de Examen Python Sistemas operativos Nivel examen Código probado

Una estación meteorológica entrega una lista de lecturas enteras de temperatura, expresadas en décimas de grado. Se quiere calcular un resumen global (suma, mínimo, máximo y número de lecturas) repartiendo el trabajo entre varios hilos, uno por trozo de la lista, y uniendo después los resultados parciales.
Entrada. La primera línea contiene dos enteros separados por un espacio: H, el número de hilos que hay que lanzar (H ≥ 1), y N, el número de lecturas (N ≥ 0). La segunda línea contiene las N lecturas separadas por espacios; si N es 0, esa línea puede estar vacía o no existir. Cada lectura es un entero con valor absoluto menor que 109.
Reparto. Las lecturas se dividen en H trozos contiguos y consecutivos, en el orden original. Sea q = N // H y r = N % H: los r primeros trozos tienen q + 1 lecturas y los H - r restantes tienen q. Si H > N, los últimos hilos reciben un trozo vacío. Se lanzan siempre exactamente H hilos, y cada uno calcula el resumen de su trozo: cuenta, suma, mínimo y máximo.
Unión. Tras esperar a todos los hilos, el programa combina los resúmenes parciales. Un trozo vacío no debe afectar al mínimo ni al máximo globales.
Salida. Primero, una línea por hilo, en orden de índice (0 a H-1), con el formato hilo i: n=cuenta suma=S min=M max=X; si el trozo es vacío se escribe hilo i: vacio. Después, una línea final total: n=cuenta suma=S min=M max=X, o total: vacio si no hay lecturas. La salida no puede depender del orden en que terminen los hilos.
Se pide un programa completo en Python que use el módulo threading y lea de la entrada estándar.
Entrada de ejemplo
3 7
12 -5 30 8 0 25 -14Salida esperada
hilo 0: n=3 suma=37 min=-5 max=30
hilo 1: n=2 suma=8 min=0 max=8
hilo 2: n=2 suma=11 min=-14 max=25
total: n=7 suma=56 min=-14 max=30- Reparto correcto en trozos contiguos (incluido el caso de que no divida exacto y de que haya más hilos que lecturas)
- 0,75
- Creación y espera (join) de todos los hilos, sin acumular en una variable global sin proteger
- 0,75
- Resumen parcial por hilo (suma, mínimo, máximo, cuenta) y unión correcta de los parciales
- 1,0
- Lectura de la entrada y formato exacto de la salida, incluido el caso vacío
- 0,5
Pistas
Pista 1 · ¿Cómo calculo dónde empieza y acaba el trozo de cada hilo?
Recorre los índices de hilo de 0 a H-1 manteniendo un puntero inicio. El tamaño del trozo del hilo i es q + 1 si i < r y q en caso contrario. El final es inicio + tamaño, y el siguiente hilo empieza donde acabó éste. Comprueba con N = 7, H = 3 que salen tamaños 3, 2 y 2.
Pista 2 · ¿Dónde guardo el resultado de cada hilo sin que se pisen?
Prepara antes de lanzar nada una lista resultados de longitud H y haz que el hilo i escriba solo en la posición i. Así no hay dos hilos tocando el mismo dato y no necesitas cerrojo. Pasa a cada hilo su índice mediante args en el constructor de threading.Thread.
Pista 3 · ¿Cómo uno los parciales y qué hago con los trozos vacíos?
Después de hacer join() a todos los hilos, recorre resultados en orden. Representa el trozo vacío como None y sáltalo al combinar. Suma las cuentas y las sumas, y toma el mínimo de los mínimos y el máximo de los máximos. Imprime siempre desde el hilo principal, nunca desde los hilos trabajadores.
Solución
Solución explicada
La solución sigue el esquema clásico de dividir, calcular y combinar. El hilo principal lee los datos, decide los límites de cada trozo, lanza H hilos y espera a que terminen todos. Cada hilo escribe su resumen parcial en su propia casilla de una lista compartida, de modo que no existe ninguna posición a la que escriban dos hilos y no hace falta sincronización explícita más allá del join.
La impresión se hace únicamente desde el hilo principal, después de la unión. Con eso la salida es determinista aunque el planificador intercale los hilos de cualquier manera.
import sys
import threading
def resumir(datos, inicio, fin, indice, resultados):
"""Calcula el resumen de datos[inicio:fin] y lo guarda en resultados[indice]."""
if inicio >= fin:
resultados[indice] = None
return
cuenta = 0
suma = 0
minimo = datos[inicio]
maximo = datos[inicio]
for k in range(inicio, fin):
v = datos[k]
cuenta += 1
suma += v
if v < minimo:
minimo = v
if v > maximo:
maximo = v
resultados[indice] = (cuenta, suma, minimo, maximo)
def formato(res):
cuenta, suma, minimo, maximo = res
return f"n={cuenta} suma={suma} min={minimo} max={maximo}"
def main():
lineas = sys.stdin.read().split("\n")
h, n = map(int, lineas[0].split())
datos = []
if n > 0:
datos = list(map(int, lineas[1].split()))
q, r = divmod(n, h)
resultados = [None] * h
hilos = []
inicio = 0
for i in range(h):
tam = q + 1 if i < r else q
fin = inicio + tam
t = threading.Thread(target=resumir,
args=(datos, inicio, fin, i, resultados))
hilos.append(t)
t.start()
inicio = fin
for t in hilos:
t.join()
salida = []
for i, res in enumerate(resultados):
if res is None:
salida.append(f"hilo {i}: vacio")
else:
salida.append(f"hilo {i}: {formato(res)}")
parciales = [res for res in resultados if res is not None]
if not parciales:
salida.append("total: vacio")
else:
cuenta = sum(p[0] for p in parciales)
suma = sum(p[1] for p in parciales)
minimo = min(p[2] for p in parciales)
maximo = max(p[3] for p in parciales)
salida.append(f"total: {formato((cuenta, suma, minimo, maximo))}")
print("\n".join(salida))
main()La función resumir es el cuerpo de cada hilo. Recibe la lista completa, los límites inicio y fin de su trozo, su indice y la lista resultados. Si el trozo está vacío guarda None; si no, recorre su rango una sola vez acumulando cuenta, suma, mínimo y máximo, y guarda la tupla en resultados[indice]. Inicializar mínimo y máximo con el primer elemento evita usar valores centinela arbitrarios.
En main, divmod(n, h) da q y r. El bucle por i calcula el tamaño del trozo (q + 1 para los r primeros) y avanza inicio hasta fin, de forma que los trozos son contiguos, cubren todo el rango y no se solapan. Cada hilo se crea y se lanza en el mismo bucle, y se guarda en hilos para poder esperarlo después. Es importante hacer los join en un segundo bucle: si se hiciera join justo tras start, los hilos se ejecutarían uno detrás de otro y se perdería el paralelismo.
La unión se hace tras los join, en el hilo principal. Se filtran los None con la lista parciales para que un trozo vacío no contamine el mínimo ni el máximo, y se combinan con sum, min y max. Como cada hilo escribe solo en su casilla, no hay carrera; la espera con join garantiza que el hilo principal lee los resultados ya completos.
Sobre los casos de prueba: en el ejemplo (H = 3, N = 7) los tamaños son 3, 2 y 2 y el total reúne las siete lecturas. En H = 5, N = 3 los hilos 3 y 4 reciben trozo vacío y se imprimen como vacio. En N = 0 todos los trozos son vacíos y el total también. Con un solo hilo, el resumen del hilo coincide con el total. Por último, un caso con valores negativos comprueba que mínimo y máximo se calculan bien y no se inicializan a cero.
Casos de prueba
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| Ejemplo: 7 lecturas en 3 hilos | 3 7
12 -5 30 8 0 25 -14 | hilo 0: n=3 suma=37 min=-5 max=30
hilo 1: n=2 suma=8 min=0 max=8
hilo 2: n=2 suma=11 min=-14 max=25
total: n=7 suma=56 min=-14 max=30 | hilo 0: n=3 suma=37 min=-5 max=30
hilo 1: n=2 suma=8 min=0 max=8
hilo 2: n=2 suma=11 min=-14 max=25
total: n=7 suma=56 min=-14 max=30 | OK |
| Más hilos que lecturas | 5 3
4 9 -2 | hilo 0: n=1 suma=4 min=4 max=4
hilo 1: n=1 suma=9 min=9 max=9
hilo 2: n=1 suma=-2 min=-2 max=-2
hilo 3: vacio
hilo 4: vacio
total: n=3 suma=11 min=-2 max=9 | hilo 0: n=1 suma=4 min=4 max=4
hilo 1: n=1 suma=9 min=9 max=9
hilo 2: n=1 suma=-2 min=-2 max=-2
hilo 3: vacio
hilo 4: vacio
total: n=3 suma=11 min=-2 max=9 | OK |
| Sin lecturas | 2 0
| hilo 0: vacio
hilo 1: vacio
total: vacio | hilo 0: vacio
hilo 1: vacio
total: vacio | OK |
| Un solo hilo | 1 4
10 20 30 40 | hilo 0: n=4 suma=100 min=10 max=40
total: n=4 suma=100 min=10 max=40 | hilo 0: n=4 suma=100 min=10 max=40
total: n=4 suma=100 min=10 max=40 | OK |
| Reparto exacto y valores negativos | 2 6
-7 -3 -9 -1 -8 -2 | hilo 0: n=3 suma=-19 min=-9 max=-3
hilo 1: n=3 suma=-11 min=-8 max=-1
total: n=6 suma=-30 min=-9 max=-1 | hilo 0: n=3 suma=-19 min=-9 max=-3
hilo 1: n=3 suma=-11 min=-8 max=-1
total: n=6 suma=-30 min=-9 max=-1 | OK |
Salidas reales: código compilado con Python 3.13.16 y ejecutado en un contenedor aislado el 15 de noviembre de 2026.
Complejidad
Cada elemento se visita exactamente una vez en alguno de los hilos, y la unión recorre H parciales, así que el trabajo total es O(N + H). Con un reparto equilibrado, el trabajo por hilo es como máximo ⌈N/H⌉ elementos. En CPython, por el GIL, los hilos no aceleran un bucle de cálculo puro: el tiempo real sigue siendo del orden de N. El ejercicio trata de la estructura del reparto y la unión, que es la misma que se usaría con procesos o con hilos en un lenguaje sin GIL.
En memoria, se guardan los N datos, la lista de H hilos y la de H resultados, es decir O(N + H). Cada hilo añade además su pila propia, por lo que lanzar miles de hilos para pocos datos es contraproducente.
Errores típicos
- Llamar a
join()justo después destart()dentro del mismo bucle: se serializa la ejecución y no hay concurrencia real. - Repartir con
N // Hsin tratar el resto: las últimasN % Hlecturas quedan sin procesar y la suma total es incorrecta. - Inicializar el mínimo a 0 y el máximo a 0 (o a un valor fijo): con lecturas todas negativas o todas positivas el resultado es erróneo. Tampoco debe un trozo vacío aportar un 0 al mínimo global.
- Hacer que cada hilo escriba en una variable global compartida (por ejemplo
total += v) sin cerrojo: es una condición de carrera y el resultado puede variar entre ejecuciones. - Imprimir desde los hilos trabajadores: el orden de las líneas depende del planificador y la salida deja de ser determinista.
- Olvidar el caso
N = 0: intentar leer la segunda línea y acceder adatos[inicio]en un trozo vacío provoca unIndexError.
Variantes
Un acumulador único protegido con cerrojo
En vez de una casilla por hilo, todos los hilos actualizan un único diccionario compartido con cuenta, suma, mínimo y máximo. Cada actualización debe hacerse dentro de with cerrojo:, siendo cerrojo = threading.Lock(), porque las operaciones de lectura, comparación y escritura no son atómicas. Para reducir la contención conviene que cada hilo calcule primero su resumen local y tome el cerrojo una sola vez, al final, para mezclarlo con el global. El resultado es el mismo, pero se añade una sección crítica; la solución con casillas independientes evita ese coste.
Reparto dinámico con una cola de tareas
Si el coste por lectura no fuera uniforme, un reparto estático desequilibraría la carga. Se trocea la lista en bloques pequeños, se meten en una queue.Queue y cada uno de los H hilos va extrayendo bloques hasta que la cola se vacía, guardando sus parciales en una lista propia. Al final se unen todos los parciales. Se gana equilibrio de carga a cambio de un poco más de gestión y de que el orden de procesado deja de estar fijado de antemano.
En los temarios
El reparto de un cálculo entre hilos y la unión de sus resultados pertenece al bloque de procesos, hilos y sincronización de las asignaturas de Sistemas Operativos. El programa de la UTN-FRBA (Argentina) incluye entre sus contenidos la planificación e hilos en procesos y la comunicación y sincronización entre procesos, y las sumillas de Ingeniería Informática de la PUCP (Perú) mencionan procesos e hilos y comunicación entre procesos.
En México, el temario de Sistemas Operativos de la ESCOM del IPN lista entre sus contenidos la administración de procesos. Este ejercicio es un ejemplo práctico para ensayar esas ideas con la biblioteca estándar de Python, sin que ello signifique que ninguno de estos centros lo plantee en sus exámenes.
Artículo generado mediante AI.larebelion



Comentarios