Tema 3 · Bucle de eventos, colas y dispatching
Programación Dirigida por Eventos · Grado en Ingeniería Informática · 2026/27
Del publicar que llama en el acto a una cola en medio: productor, cola, bucle y handler; FIFO y prioridades con el panel de una caldera; run-to-completion, coalescencia, colas llenas, handlers largos y el Looper de Android.
De la llamada directa a la cola
La barra del bar en Kotlin (productor, cola, bucle y handler), run-to-completion y la ficha de traza del panel de una caldera recorrida con cola FIFO y con cola de prioridad.
Teoría 1. ¿Y si llegan tres eventos mientras el programa está ocupado?
Hasta ahora, quien producía un evento esperaba a que se atendiera. Repasa cómo lo hemos ido viendo:
U1 · Fuente, evento, receptor
Un while que obtiene un evento y lo entrega. Solo tenía uno en la mano: para el siguiente había que esperar al teclado.
U2 · Callbacks y despacho
publicar llamaba en el acto a los handlers. Quien publicaba esperaba hasta que el último terminaba.
U3 · Una cola en medio
Quien produce no espera: deja el evento y se va. Alguien lo atiende después, cuando le toque.
Lo que dejamos pendiente en la U2
En la clase 5 de la U2 arreglamos la reentrancia de la central de pedidos cambiando una sola palabra: CentralConCola. Su publicar ya llevaba dentro una cola y un while:
fun publicar(estado: String) {
pendientes.addLast(estado)
if (notificando) return
notificando = true
try {
while (pendientes.isNotEmpty()) {
val siguiente = pendientes.removeFirst()
for (o in oyentes.toList()) {
o(siguiente)
}
}
} finally {
notificando = false
}
}pendientesguarda los avisos que llegan durante una ronda.- La vacía quien publicó primero: se queda dentro hasta entregarlo todo.
- El
whileentrega los avisos de uno en uno: cada ronda termina antes de que empiece la siguiente.
La pregunta con la que cerramos la U2 era: ¿y si quien publica dejara el aviso y se fuera? Esa es exactamente la idea de esta unidad.
publicar que llama a los handlers en el acto, ¿cuándo puede seguir quien publica?- En cuanto deja el evento en la cola
- Cuando el último handler ha terminado
- Cuando el primer handler ha empezado
- Nunca: publicar no devuelve el control
Llamada directa = esperar. publicar recorre los handlers y solo devuelve el control cuando el último ha terminado. Dejar el evento y seguir es justo lo que aporta la cola.
CentralConCola, ¿quién vacía la cola pendientes?- Un hilo aparte que la revisa cada cierto tiempo
- Cada suscriptor, cuando termina su aviso
- Quien publicó primero: se queda en el
whilehasta entregarlo todo - Nadie: los avisos que llegan durante una ronda se descartan
El primero que publica pone notificando = true y entra en el while. Los que publican durante la ronda encolan y se van (return). En la U3 damos el paso siguiente: que nadie que publique se quede a vaciar la cola, sino un bucle dedicado.
Teoría 2. La barra del bar: cuatro piezas
Una cola de eventos mínima tiene cuatro papeles. En la barra de un bar se ven muy bien:
Quien pide no espera a que le preparen: deja la comanda y se va. Si en la cola hay bocadillo, zumo y cafe (el café llegó primero), lo siguiente que sale es el café, y el bocadillo, que entró el último, saldrá el último.
Tres reglas
| Regla | En el bar |
|---|---|
| 1 · FIFO | La primera comanda que entró es la primera que sale. |
| 2 · De una en una | El camarero termina una comanda entera antes de coger la siguiente. |
| 3 · Se anota, no se prepara | Si al preparar hace falta algo más, se anota al final de la fila; no se prepara en el acto. |
Del bar al programa
Los mismos cuatro papeles, con otros nombres:
| En el bar | En el programa | En Comandas.kt |
|---|---|---|
| Quien pide | Productor | pedir(comanda) |
| La fila de comandas | Cola de eventos | comandas (ArrayDeque) |
| El camarero que coge la primera y vuelve a mirar | Bucle de eventos | atenderBarra() |
| Preparar una comanda | Handler | preparar(comanda) |
Productor → cola → bucle → handler. El productor no espera a nadie: deja la comanda y se va.
Comandas.kt, ¿qué función hace el papel de bucle de eventos? (solo el nombre)atenderBarraatenderBarra() es el bucle: mientras haya comandas, saca la más antigua y se la da a preparar, que es el handler. pedir es el productor y comandas la cola.
- Prepara el café en el acto y luego sigue con el desayuno
- Anota el café al final de la fila
- Anota el café al principio de la fila para que salga el siguiente
- Deja el desayuno a medias y atiende la siguiente comanda
Lo que haga falta mientras se prepara va al final de la fila. Prepararlo en el acto sería volver a la llamada directa; dejar el desayuno a medias rompería la regla 2 (de una en una).
Práctica 3. PASO 1 · Llamada directa: pedir es preparar
En clase lo tecleamos en vivo en pde-consola, en un fichero Comandas.kt. Empezamos por el punto de partida, como en la U2: quien pide llama directamente al handler.
fun preparar(comanda: String) {
when (comanda) {
else -> println(" preparado: $comanda")
}
}
fun main() {
println("PASO 1 - llamada directa")
preparar("cafe")
preparar("zumo")
}Salida:
PASO 1 - llamada directa
preparado: cafe
preparado: zumoDos cosas que hay que ver aquí:
- Pedir es preparar. Quien llama a
prepararespera a que termine. No hay forma de "dejar la comanda e irse". - Igual que en la U2.
publicarllamaba al handler en el acto; aquímainllama aprepararen el acto.
El when con un solo else parece raro, pero está preparado para crecer: en el PASO 3 le añadiremos un caso especial.
Si tienes pde-consola a mano, crea Comandas.kt con este código y ejecútalo: en los dos pasos siguientes lo iremos ampliando sin borrar nada.
- Nada: el zumo se prepara a la vez que el café
- Esperaría esos dos minutos antes de poder pedir
- El zumo se perdería
- El zumo saldría antes porque es más rápido de preparar
Con llamada directa todo es secuencial y bloqueante: preparar("zumo") no empieza hasta que preparar("cafe") devuelve el control.
- El productor y el handler
- La cola y el bucle
- Solo el handler
- No falta ninguna
Hay quien pide (main) y quien prepara (preparar), pero no hay cola donde dejar la comanda ni bucle que la saque. Es lo que añade el PASO 2.
Práctica 4. PASO 2 · Una cola y un bucle
Encima de lo anterior añadimos las tres piezas que faltaban: la cola, quien anota y quien atiende.
val comandas = ArrayDeque<String>()
fun pedir(comanda: String) {
comandas.addLast(comanda)
println(" anotado: $comanda (en cola: ${comandas.size})")
}
fun atenderBarra() {
while (comandas.isNotEmpty()) {
val comanda = comandas.removeFirst()
preparar(comanda)
}
println(" (cola vacia: el bucle se queda esperando)")
}
fun main() {
println("PASO 2 - cola y bucle")
pedir("cafe")
pedir("zumo")
pedir("bocadillo")
atenderBarra()
}- La cola (
comandas): lo pedido y todavía no preparado. - Entra por el final (
addLast): quien pide anota y se va. - Sale por el principio (
removeFirst): la más antigua primero. Eso es FIFO.
atenderBarra no decide nada: mientras haya comandas, saca la más antigua y se la entrega al handler.
Salida:
PASO 2 - cola y bucle
anotado: cafe (en cola: 1)
anotado: zumo (en cola: 2)
anotado: bocadillo (en cola: 3)
preparado: cafe
preparado: zumo
preparado: bocadillo
(cola vacia: el bucle se queda esperando)- Primero se anota todo: las tres líneas de «anotado» salen antes que ningún «preparado». Nadie ha esperado.
- Después, en orden de llegada: se prepara en el mismo orden en que se pidió.
- Y el bucle se queda sin trabajo: aquí termina porque es un programa de consola.
ArrayDeque hace que salga siempre la comanda más antigua? (solo el nombre del método)removeFirstaddLast mete por el final y removeFirst saca por el principio: la combinación da FIFO. Si sacases con removeLast tendrías una pila (LIFO).
atenderBarra cambiases removeFirst() por removeLast(), ¿en qué orden saldrían los «preparado»?- cafe, zumo, bocadillo
- bocadillo, zumo, cafe
- zumo, cafe, bocadillo
- Solo saldría bocadillo
Con removeLast la estructura se comporta como una pila (LIFO): sale primero lo último que entró. El bucle seguiría igual; lo que cambia el orden es la cola, no el bucle.
Ejercicio 5. PASO 3 · Un handler que encola (predice antes de ejecutar)
Un desayuno es un café y una tostada. Cambiamos el handler para que, al recibir un desayuno, pida las dos cosas:
fun preparar(comanda: String) {
when (comanda) {
"desayuno" -> {
pedir("cafe")
pedir("tostada")
println(" desayuno: repartido en dos comandas")
}
else -> println(" preparado: $comanda")
}
}
// en main
pedir("desayuno")
pedir("zumo")
atenderBarra()Antes de ejecutar
Hay un zumo esperando detrás del desayuno. En clase se votó a mano alzada: ¿qué sale antes, el café del desayuno o el zumo?
Recorre la cola a mano. Te ayuda apuntar en un papel cómo queda comandas después de cada línea:
| Momento | Cola después |
|---|---|
pedir("desayuno") |
[desayuno] |
pedir("zumo") |
[desayuno, zumo] |
| el bucle saca el desayuno | ? |
| el handler pide café y tostada | ? |
| ... | ... |
Pista para el número «en cola»: cuando el handler del desayuno empieza, el desayuno ya ha salido de la cola.
No sigas hasta tener tu predicción completa: en el paso siguiente verás la salida real.
- El café del desayuno
- El zumo
- Salen a la vez
- Depende de cuál tarde menos
El café se anota al final de la cola, detrás del zumo, que ya estaba esperando. Así que se prepara antes el zumo.
El handler del desayuno se ejecuta entero (anota café, anota tostada, imprime su línea) y solo entonces el bucle coge la siguiente comanda, que es el zumo. Café y tostada quedaron detrás.
Teoría 6. Sale antes el zumo: run-to-completion
Esta es la salida real del PASO 3:
PASO 3 - un handler que encola
anotado: desayuno (en cola: 1)
anotado: zumo (en cola: 2)
anotado: cafe (en cola: 2)
anotado: tostada (en cola: 3)
desayuno: repartido en dos comandas
preparado: zumo
preparado: cafe
preparado: tostada
(cola vacia: el bucle se queda esperando)- El café y la tostada van al final. Se anotan detrás del zumo, que ya estaba en la cola.
- El handler termina entero. «desayuno: repartido…» sale antes que cualquier «preparado»: solo cuando el handler del desayuno acaba, el bucle coge la comanda siguiente.
- Con llamada directa, al revés. Si el handler llamase a
preparar("cafe"), el café se habría preparado dentro del desayuno, antes que el zumo.
Esto es lo que diferencia de verdad la cola de la llamada directa: con cola, cada evento se atiende de principio a fin y los eventos nuevos no se cuelan. El orden de ejecución pasa a depender solo del orden en que las cosas entran en la cola.
Llamada directa
Un handler dentro de otro: el café se prepara mientras el desayuno sigue a medias.
Cola + bucle
El desayuno termina; el café espera detrás del zumo.
preparar("cafe") en lugar de pedir("cafe"), ¿qué se imprimiría antes?- «preparado: zumo»
- «preparado: cafe»
- «desayuno: repartido en dos comandas»
- «anotado: cafe»
Con llamada directa el café se prepara dentro del desayuno: «preparado: cafe» sale antes que la línea del desayuno y antes que el zumo. Un handler dentro de otro.
- Un handler se ejecuta entero antes de que el bucle saque el siguiente evento
- Un evento encolado por un handler puede interrumpirlo si es urgente
- Lo que produce un handler espera su turno en la cola
- Garantiza que todos los eventos se atienden en el mismo instante en que ocurren
Nada interrumpe al handler que se está ejecutando, ni siquiera algo urgente; lo nuevo va a la cola. Y precisamente por eso los eventos no se atienden cuando ocurren, sino cuando les toca.
Teoría 7. El panel de una caldera (Anexo 0)
Para la Actividad 1 cambiamos de escenario: el panel de una caldera recibe tres tipos de evento. Cada uno tiene una notación corta (la que usaremos en la ficha), una prioridad y la línea que muestra el handler al atenderlo.
| Evento | Notación | Prioridad | Línea que muestra el handler |
|---|---|---|---|
Pulsacion(boton) |
P:SUBIR |
NORMAL | Pulsado SUBIR |
Lectura(grados) |
L21 |
BAJA | Lectura 21 grados |
Emergencia(motivo) |
E:humo |
ALTA | EMERGENCIA: humo |
L21 es una lectura de 21 grados, P:SUBIR una pulsación del botón SUBIR y E:humo una emergencia por humo. En la ficha, el primero de la lista es el siguiente en salir.
El handler, tal como está en PanelEventos.kt:
fun manejarPanel(evento: EventoPanel, publicar: (EventoPanel) -> Unit): String =
when (evento) {
is Pulsacion -> "Pulsado ${evento.boton}"
is Lectura -> {
if (evento.grados >= 30) publicar(Emergencia("temperatura ${evento.grados}"))
"Lectura ${evento.grados} grados"
}
is Emergencia -> "EMERGENCIA: ${evento.motivo}"
}Recibe un evento y devuelve la línea que muestra el panel. El parámetro publicar es una función: es la manera que tiene el handler de producir un evento nuevo sin atenderlo él.
Y las prioridades:
enum class PrioridadPanel { ALTA, NORMAL, BAJA } // cuanto más arriba, antes se atiende
fun prioridadDelEvento(evento: EventoPanel): PrioridadPanel =
when (evento) {
is Emergencia -> PrioridadPanel.ALTA
is Pulsacion -> PrioridadPanel.NORMAL
is Lectura -> PrioridadPanel.BAJA
}manejarPanel al atender L31?Lectura 31 gradosDevuelve «Lectura 31 grados». Además, como 31 ≥ 30, antes de devolverla llama a publicar(Emergencia("temperatura 31")): produce la emergencia, pero no la atiende.
prioridadDelEvento, ¿qué prioridad tiene una pulsación?- ALTA
- NORMAL
- BAJA
- Depende del botón
Emergencia → ALTA, Pulsacion → NORMAL, Lectura → BAJA. El botón no influye.
publicar de manejarPanel?- Un booleano que indica si hay que imprimir la línea
- Una función que el handler llama para producir un evento nuevo
- La cola de eventos
- La prioridad del evento
Su tipo es (EventoPanel) -> Unit: una función que recibe un evento. El handler no sabe qué hace esa función por dentro; eso lo decide quien le llama (el bucle).
Ejercicio 8. Sin cola, la emergencia sale antes que su lectura
Primero lo vemos sin cola. En PanelDirecto.kt cada evento se entrega al handler en el acto: quien produce el evento llama al handler y espera. Con los eventos del guion de la actividad, la salida es:
Panel con llamada directa (sin cola)
Lectura 21 grados
Pulsado SUBIR
EMERGENCIA: temperatura 31
Lectura 31 grados
Pulsado MODO
EMERGENCIA: humoTres cosas que observar:
- Entrega en el acto. Los eventos salen exactamente en el orden en que se producen; no hay nada que reordenar.
- La emergencia sale antes. «EMERGENCIA: temperatura 31» aparece antes que la «Lectura 31 grados» que la provoca.
- Un handler dentro de otro. El handler de la emergencia se ejecuta dentro del de la lectura:
manejarPanel(L31)llama apublicar, y con llamada directapublicaratiende la emergencia en el acto. La línea de la emergencia sale y, cuando vuelve, la lectura termina y devuelve su propia línea.
Es el mismo fenómeno que la reentrancia de la U2: el orden de la salida no coincide con el orden lógico de los hechos (primero se lee 31 grados, luego hay emergencia).
PanelDirecto.kt «EMERGENCIA: temperatura 31» sale antes que «Lectura 31 grados»?- Porque las emergencias tienen prioridad ALTA
- Porque
publicaratiende la emergencia en el acto, antes de que el handler de la lectura devuelva su línea - Porque el sensor envía la emergencia antes que la lectura
- Porque la cola FIFO la coloca delante
No hay cola ni prioridad que valga: manejarPanel(L31) llama a publicar antes de devolver «Lectura 31 grados», y esa llamada ejecuta entero el handler de la emergencia. Un handler dentro de otro.
manejarPanel hay en marcha a la vez? (un número)2Dos: la de la lectura (a medias, esperando a que publicar vuelva) y la de la emergencia. Con cola esto nunca pasa: run-to-completion garantiza que solo hay una en marcha.
Ejercicio 9. Actividad 1 · Parte A: cola FIFO
En clase, en pareja y en papel: 9 minutos con la cola FIFO y 8 con la de prioridad (Anexo 1, ficha de traza).
El guion: 11 pasos
| Paso | Operación |
|---|---|
| 1 a 3 | encolar L21, P:SUBIR, L31 |
| 4 | atender uno |
| 5 y 6 | encolar P:MODO, E:humo |
| 7 a 11 | atender uno, hasta vaciar la cola |
En cada fila escribe cómo queda la cola después del paso (el primero de la lista es el siguiente en salir) y, si se atiende un evento, qué línea sale. En «atender uno» se atiende uno solo. No olvides la regla del handler: una lectura de 30 grados o más publica una emergencia (E:t31).
Parte A · Cola FIFO: se atiende en orden de llegada.
| Paso | Operación | Cola después | Línea que sale |
|---|---|---|---|
| 1 | encolar L21 | [L21] |
|
| 2 | encolar P:SUBIR | ||
| 3 | encolar L31 | ||
| 4 | atender uno | ||
| 5 | encolar P:MODO | ||
| 6 | encolar E:humo | ||
| 7–11 | atender uno |
Rellénala a mano y después compruébala con el simulador (modo FIFO, paso a paso):
FIFO: L21, P:SUBIR, L31, P:MODO, E:humo, E:t31. La emergencia de temperatura entra al final cuando se atiende L31 (paso 8), detrás de P:MODO y E:humo.
E:humo con la cola FIFO? (un número)5Se atiende la quinta, en el paso 10. Una emergencia, esperando detrás de pulsaciones y lecturas.
E:humo llega a la cola (paso 6), ¿cuántos eventos tiene delante?3Tres: P:SUBIR, L31 y P:MODO. La cola tras el paso 6 es [P:SUBIR, L31, P:MODO, E:humo].
Ejercicio 10. Parte A resuelta y preguntas A2 y A3
Compara tu ficha con la solución de la Parte A:
| Paso | Operación | Cola después | Línea que sale |
|---|---|---|---|
| 1 | encolar L21 | [L21] |
— |
| 2 | encolar P:SUBIR | [L21, P:SUBIR] |
— |
| 3 | encolar L31 | [L21, P:SUBIR, L31] |
— |
| 4 | atender uno | [P:SUBIR, L31] |
Lectura 21 grados |
| 5 | encolar P:MODO | [P:SUBIR, L31, P:MODO] |
— |
| 6 | encolar E:humo | [P:SUBIR, L31, P:MODO, E:humo] |
— |
| 7 | atender uno | [L31, P:MODO, E:humo] |
Pulsado SUBIR |
| 8 | atender uno | [P:MODO, E:humo, E:t31] |
Lectura 31 grados |
| 9 | atender uno | [E:humo, E:t31] |
Pulsado MODO |
| 10 | atender uno | [E:t31] |
EMERGENCIA: humo |
| 11 | atender uno | [] |
EMERGENCIA: temperatura 31 |
Fíjate en la fila 8: al atender L31, la cola pierde L31 y gana E:t31 al final. La línea que sale es la de la lectura; la emergencia tendrá que esperar a que salgan los que ya estaban (P:MODO y E:humo).
Compara también con PanelDirecto.kt: allí la emergencia de temperatura salía antes que su lectura; aquí sale la última, después de su lectura.
E:temperatura 31 y quién la encola?- En el paso 3, la encola quien encola
L31 - En el paso 8, la encola el handler de
L31(a través depublicar) - En el paso 8, la encola el bucle porque detecta los 31 grados
- En el paso 11, la encola el handler de
E:humo
Entra en el paso 8, cuando se atiende L31. La encola el handler de la lectura llamando a publicar. El bucle no sabe nada de grados: solo saca y entrega.
PanelDirecto.kt esa emergencia salía antes que su lectura. ¿Por qué con cola sale después?- Porque la cola FIFO da menos prioridad a las emergencias
- Porque el handler de la lectura publica (encola) la emergencia y termina; la emergencia espera su turno en la cola
- Porque la cola invierte el orden de los eventos
- Porque con cola el handler ya no publica la emergencia
Con cola, publicar no atiende: encola. El handler de L31 termina entero y devuelve «Lectura 31 grados»; la emergencia espera en la cola y sale cuando le toca. Ya no hay un handler dentro de otro.
Ejercicio 11. Actividad 1 · Parte B: cola con prioridad
Mismo guion, mismos eventos, mismo bucle y mismo handler. Lo único que cambia es la cola.
Parte B · Cola con prioridad: ALTA antes que NORMAL, NORMAL antes que BAJA. Entre iguales, orden de llegada.
| Prioridad | Eventos |
|---|---|
| ALTA | E:humo, E:t31 |
| NORMAL | P:SUBIR, P:MODO |
| BAJA | L21, L31 |
Cómo se trabaja en la ficha: al encolar, el evento se coloca delante del primero que sea menos urgente que él; si hay otros de su misma prioridad, se queda detrás de ellos. «Atender uno» saca siempre el primero de la lista.
| Paso | Operación | Cola después | Línea que sale |
|---|---|---|---|
| 1 | encolar L21 | [L21] |
|
| 2 | encolar P:SUBIR | ||
| 3 | encolar L31 | ||
| 4 | atender uno | ||
| 5 | encolar P:MODO | ||
| 6 | encolar E:humo | ||
| 7–11 | atender uno |
Rellena la columna y comprueba con el simulador, ahora en modo prioridad:
Prioridad: P:SUBIR (paso 4, la más urgente de las que había), E:humo, P:MODO, L21, L31 y E:t31. La emergencia de temperatura nace en el paso 10 al atender L31 y es lo único que queda.
E:humo? (un número)2La segunda: en el paso 4 ya se ha atendido P:SUBIR; cuando E:humo llega en el paso 6 se pone delante de todo y sale en el paso 7.
Ejercicio 12. Parte B resuelta y preguntas B2 a B4
Solución de la Parte B:
| Paso | Operación | Cola después | Línea que sale |
|---|---|---|---|
| 1 | encolar L21 | [L21] |
— |
| 2 | encolar P:SUBIR | [P:SUBIR, L21] |
— |
| 3 | encolar L31 | [P:SUBIR, L21, L31] |
— |
| 4 | atender uno | [L21, L31] |
Pulsado SUBIR |
| 5 | encolar P:MODO | [P:MODO, L21, L31] |
— |
| 6 | encolar E:humo | [E:humo, P:MODO, L21, L31] |
— |
| 7 | atender uno | [P:MODO, L21, L31] |
EMERGENCIA: humo |
| 8 | atender uno | [L21, L31] |
Pulsado MODO |
| 9 | atender uno | [L31] |
Lectura 21 grados |
| 10 | atender uno | [E:t31] |
Lectura 31 grados |
| 11 | atender uno | [] |
EMERGENCIA: temperatura 31 |
Mismos eventos, otra cola, otro orden
| Cola | Orden de atención |
|---|---|
| FIFO | L21, P:SUBIR, L31, P:MODO, E:humo, E:temperatura 31 |
| Prioridad | P:SUBIR, E:humo, P:MODO, L21, L31, E:temperatura 31 |
E:humo
Con FIFO, la quinta: esperó detrás de tres. Con prioridad, la segunda.
L21
Alguien lo paga: llegó la primera y con prioridad se atiende la cuarta.
E:temperatura 31
La última en las dos colas, siempre después de su lectura.
L21L21: llegó la primera y con FIFO se atendía la primera; con prioridad baja al cuarto puesto porque todas las pulsaciones y emergencias le adelantan.
E:humo, ¿interrumpe al handler de algún otro evento?- Sí, interrumpe al handler de P:SUBIR
- Sí, interrumpe al handler de cualquier evento de prioridad menor
- No: adelanta en la cola, pero ningún handler se interrumpe (run-to-completion)
- No, porque se descarta hasta que la cola se vacía
La prioridad decide qué sale de la cola, no corta lo que se está ejecutando. Cada handler termina entero; E:humo simplemente se coloca la primera para la siguiente vez que el bucle saque un evento.
L21?- Al cabo de 11 pasos, como en el guion
- En cuanto fuese la más antigua de la cola
- Nunca: siempre habría algo más urgente delante (inanición)
- Inmediatamente, porque las lecturas tienen la prioridad más alta
Nunca: con prioridad estricta, un flujo continuo de eventos más urgentes deja sin atender a los de menos prioridad. Se llama inanición (starvation) y en la sesión de obligatorios veremos cómo evitarla.
Teoría 13. Cierre: tres ideas y una pregunta para la Hora 2
Tres ideas que te llevas
1 · La cola separa
...cuándo ocurre un evento de cuándo se atiende. Quien produce deja el evento y sigue.
2 · De uno en uno y hasta el final
El bucle saca un evento, el handler lo termina entero (run-to-completion) y solo entonces se saca el siguiente.
3 · El orden lo decide la cola
Mismo bucle y mismo handler: con FIFO o con prioridad, el orden de atención cambia.
La ficha es la especificación de la Hora 2
La ficha rellena es tuya: guárdala. En la Hora 2 recibes el programa completo y imprime lo que hoy has escrito a mano. Antes de la Hora 2:
- Copia en tu pde-consola los cuatro archivos de
TRAB_U3-1_alumno.zip. - Comprueba que
ColaPanel.ktse ejecuta.
Para pensar
El handler manejarPanel recibe un parámetro que se llama publicar. ¿Qué le pasa el bucle ahí? Búscalo en ColaPanel.kt.
Piensa en lo que has visto hoy: en PanelDirecto.kt, publicar una emergencia equivalía a atenderla en el acto (un handler dentro de otro). En la ficha, en cambio, la emergencia entraba en la cola y esperaba. Lo que el handler recibe en publicar es lo que decide cuál de los dos comportamientos tienes, sin cambiar ni una línea del handler.
manejarPanel como publicar para que la emergencia salga después de su lectura?- Una función que llama a
manejarPanelcon el evento nuevo - Una función que mete el evento nuevo en la cola
- Una función que imprime el evento nuevo con
println - Una función vacía que ignora el evento
Para que se respete run-to-completion, publicar debe encolar el evento (en la misma cola del bucle). Si llamase al handler sería PanelDirecto.kt; si lo ignorase, la emergencia se perdería. Compruébalo en ColaPanel.kt.
- La cola separa cuándo ocurre un evento de cuándo se atiende
- El bucle atiende de uno en uno y hasta el final
- Un evento urgente interrumpe al handler en curso
- El orden lo decide la cola
- El orden lo decide el bucle
Separar ocurrencia y atención, run-to-completion y «el orden lo decide la cola». Ni lo urgente interrumpe, ni el bucle decide el orden: el bucle solo saca y entrega.
Repaso 14. Repaso: de la llamada directa a la cola
| Situación | Dónde sale el evento publicado por un handler |
|---|---|
| Llamada directa | Dentro del handler que lo publica (antes que la línea de este) |
| Cola FIFO | Al final de la cola, después de todo lo que esperaba |
| Cola con prioridad | Delante de los menos urgentes, pero siempre después de que termine el handler actual |
Responde como si fuera el parcial.
[P:MODO, L35]. Se atiende uno y llega E:humo. ¿Cómo queda la cola?[E:humo, L35][L35, E:humo][L35, E:t35, E:humo][E:humo, L35, E:t35]
Sale P:MODO (Pulsado MODO), que no publica nada, y E:humo se pone al final: [L35, E:humo]. E:t35 aparecerá cuando se atienda L35.
L40, P:SUBIR y E:humo, y después se atienden los tres. ¿Qué evento se atiende el último?L40Orden: E:humo (ALTA), P:SUBIR (NORMAL) y L40 (BAJA). Al atender L40 se publica E:t40, que quedaría en la cola para un cuarto «atender».
- Con cola, quien produce un evento no espera a que se atienda
- Con cola con prioridad, una emergencia corta el handler de una lectura en curso
- Si un handler encola un evento, ese evento no empieza hasta que el handler termina
- Cambiar FIFO por prioridad exige reescribir el handler
La cola desacopla productor y handler, y run-to-completion hace que lo encolado espere. La prioridad no interrumpe, y en la actividad el handler y el bucle no cambiaron: solo la cola.
Bucle de eventos, colas y dispatching
Las piezas del event loop, por qué la cola absorbe ráfagas pero no sobrecargas, prioridad, coalescencia y descarte, run-to-completion y handlers largos, y cómo se ve todo dentro de Android (Looper, Handler, MessageQueue).
Teoría 1. La idea de la unidad: la cocina del restaurante
La sesión responde a tres preguntas: de dónde sale la secuencia de eventos, cómo se despachan y qué pasa si un handler tarda.
| Bloque | Qué se ve |
|---|---|
| 1 · Productores, cola y bucle | Las piezas del sistema; orden, prioridad, fusión y descarte. |
| 2 · Run-to-completion | Cada evento termina antes del siguiente; consecuencias de un handler largo. |
| 3 · Android | Looper, Handler y MessageQueue; el presupuesto de 16 ms por fotograma. |
La cocina del restaurante
Varios camareros dejan comandas en el pasador y un solo cocinero las prepara de una en una. Llega la comanda de la mesa 3 y después la de la mesa 7: el cocinero coge la primera, cocina la mesa 3 entera (la 7 espera) y luego coge la siguiente.
Camareros
Los productores: toman nota y vuelven a su mesa, sin esperar.
Pasador
La cola: guarda las comandas por orden de llegada.
Cocinero
El bucle: coge una, la termina y coge la siguiente.
Receta
El handler: lo que se hace con cada tipo de comanda.
Quien produce y quien atiende no se esperan: el pasador absorbe la diferencia de ritmo entre ellos.
- En el instante exacto en que ocurre el evento
- Cuando le toca, según lo decida la cola
- Cuando el productor termina de producir todos sus eventos
- Cuando el handler anterior lo llama
Entre que algo ocurre y que se atiende está la cola. El momento de ejecución lo decide ella (orden de llegada, prioridad...), no el instante del suceso.
- El productor
- La cola
- El bucle
- El handler
La receta es el handler: la lógica concreta para cada tipo. El cocinero (bucle) solo coge comandas y aplica la receta que toque.
Teoría 2. Las piezas del sistema
Los productores generan eventos, la cola los guarda, el bucle los saca y el dispatcher los entrega a su handler.
| Pieza | Responsabilidad | Regla |
|---|---|---|
| Evento / mensaje | Tipo + datos propios de ese tipo (+ instante en que ocurrió) | Pequeño e inmutable: es un aviso, no un trabajo |
| Productor | Detecta algo y encola un evento | Rápido: no ejecuta la reacción |
| Cola | Guarda los eventos hasta que les toca | Define orden, prioridad y qué se descarta |
| Bucle + dispatcher | Saca el siguiente evento y elige el handler por su tipo | Un único hilo lo recorre |
Algunas consecuencias que conviene tener claras:
- Una ISR (rutina de interrupción) de un pulsador es un productor típico: debe ser cortísima, así que solo encola el evento y sale. La reacción la hará después el handler, fuera de la interrupción.
- El evento lleva datos, no trabajo: «pulsador 4», «sensor 24 C». Lo que hay que hacer con él lo sabe el handler.
- Como un único hilo recorre el bucle, los handlers nunca se ejecutan a la vez entre sí.
- El productor debe ser rápido y no ejecutar la reacción
- El evento debe llevar dentro el trabajo que hay que hacer
- La cola define orden, prioridad y qué se descarta
- Varios hilos recorren el bucle para ir más rápido
El evento es un aviso pequeño e inmutable (no un trabajo) y el bucle lo recorre un único hilo. Productor rápido y cola que decide orden, prioridad y descarte son las reglas correctas.
- Por su prioridad
- Por su tipo
- Por el instante en que ocurrió
- Por el productor que lo generó
El dispatcher mira el tipo del evento y elige su handler. La prioridad la usa la cola para decidir el orden de salida, no el dispatcher.
Ejercicio 3. Sensor sin cola: ¿cuántos se pierden?
Un sensor de puerta genera un evento cada 10 ms y el handler tarda 25 ms. No hay cola: si el handler está ocupado cuando llega un evento, no hay sitio donde esperar.
const llegadas = [0, 10, 20, 30, 40]; // ms: un evento cada 10 ms
const duracion = 25; // ms que tarda el handler
let libreEn = 0;
for (const t of llegadas) {
if (t < libreEn) console.log(`t=${t}: PERDIDO (handler ocupado)`);
else { console.log(`t=${t}: atendido`); libreEn = t + duracion; }
}libreEn es el instante en que el handler vuelve a estar libre. Solo se atiende el evento que llega con el handler libre; al atenderlo, el handler queda ocupado otros 25 ms.
Haz la traza tú: para cada t, compara con libreEn y actualízalo si se atiende.
| t | ¿t < libreEn? | Resultado | libreEn después |
|---|---|---|---|
| 0 | 0 < 0: no | atendido | 25 |
| 10 | ? | ? | ? |
| 20 | ? | ? | ? |
| 30 | ? | ? | ? |
| 40 | ? | ? | ? |
- t=0
- t=10
- t=20
- t=30
- t=40
t=0 se atiende (libre en 25). t=10 y t=20 llegan con el handler ocupado: perdidos. t=30 ≥ 25: atendido (libre en 55). t=40 < 55: perdido. 3 de 5 desaparecen y nadie se entera.
- Una pila, sacando el último que llegó
- Una cola, sacando por orden de llegada (FIFO)
- Una variable que guarde solo el último evento
- Nada: basta con hacer el handler más rápido que 10 ms
Una cola FIFO da a cada evento un sitio donde esperar su turno. Hacer el handler más rápido también ayudaría, pero sin cola cualquier ráfaga seguiría perdiendo eventos; y una variable con el último valor solo sirve para eventos de estado.
Ejercicio 4. Una cola entre medias: la pérdida se convierte en espera
La salida del sensor sin cola fue:
t=0: atendido
t=10: PERDIDO (handler ocupado)
t=20: PERDIDO (handler ocupado)
t=30: atendido
t=40: PERDIDO (handler ocupado)Ahora ponemos una cola entre medias:
const llegadas = [0, 10, 20, 30, 40];
const duracion = 25;
const cola = [...llegadas];
let reloj = 0;
while (cola.length) {
const t = cola.shift();
reloj = Math.max(reloj, t);
console.log(`evento t=${t}: atendido en ${reloj} (espera ${reloj - t} ms)`);
reloj += duracion;
}evento t=0: atendido en 0 (espera 0 ms)
evento t=10: atendido en 25 (espera 15 ms)
evento t=20: atendido en 50 (espera 30 ms)
evento t=30: atendido en 75 (espera 45 ms)
evento t=40: atendido en 100 (espera 60 ms)- Los eventos esperan su turno en vez de perderse.
- Se sacan por orden de llegada, uno cada vez (
shiftsaca el primero). - El precio es la espera: crece 15 ms por evento, porque el handler tarda 25 ms y el productor genera uno cada 10 ms.
Juega con el periodo del productor y la duración del handler y observa perdidos (sin cola) y esperas (con cola):
75El reloj llega a 100 + 25 = 125, así que el evento de t=50 se atiende en 125: espera 75 ms. La espera sigue creciendo 15 ms por evento.
- Crece sin parar, igual que antes
- Ningún evento espera: cada uno se atiende al llegar
- Se pierde uno de cada dos eventos
- La espera crece 25 ms por evento
Cada evento llega justo cuando el handler queda libre: espera 0 y la cola nunca acumula. La espera crece solo cuando el handler es más lento que el ritmo de llegada. Puedes comprobarlo en el simulador.
Teoría 5. La cola absorbe ráfagas, no sobrecargas
Una cola que se vacía a tiempo es una cola sana; una cola que solo crece es un sistema mal dimensionado.
| Situación | Qué hace la cola | Qué se nota |
|---|---|---|
| Ráfaga corta: llegan 5 eventos juntos y luego silencio | Los guarda y el handler los va sacando | Una espera pasajera; la cola vuelve a quedar vacía |
| Sobrecarga sostenida: cada 10 ms llega uno y el handler tarda 25 ms | Crece sin parar | La espera aumenta evento a evento: 0, 15, 30, 45, 60 ms… |
| Cola con límite: la sobrecarga continúa y se llena | Ya no cabe nada más | Hay que descartar, y decidir qué |
Cuándo la cola ayuda
Cuando el trabajo medio que llega por segundo es menor que lo que el handler puede atender, aunque llegue a golpes: cargas de pantalla, muchos clics seguidos, mensajes de red agrupados.
Cuándo no arregla nada
Cuando el productor es más rápido que el handler de forma permanente. La cola solo aplaza el problema y gasta memoria. Hace falta otro remedio: un handler más rápido, trabajo en paralelo o frenar al productor.
- Muchos clics seguidos y luego el usuario se queda leyendo
- Un sensor que envía cada 10 ms a un handler que tarda 25 ms, día y noche
- Mensajes de red que llegan agrupados de vez en cuando
- La carga de una pantalla que genera varios eventos de golpe
Las tres situaciones de ráfaga tienen un ritmo medio asumible. El sensor permanente es una sobrecarga sostenida: la cola solo aplaza el problema.
- Que la cola tiene más de 5 eventos en algún momento
- Que la longitud de la cola y la latencia crecen sin parar
- Que algún evento espera más de 0 ms
- Que el handler tarda más de 10 ms
En una ráfaga la cola sube y luego vuelve a vaciarse. Si longitud y latencia crecen sin parar, el handler no da abasto de forma permanente.
Ejercicio 6. El bucle completo con tabla de handlers (predice)
Productores que encolan, un bucle que saca por orden y un dispatcher que elige el handler por tipo. Todo cabe en unas líneas de C++:
#include <cstdio>
#include <queue>
enum Tipo { PULSADOR, TICK, SENSOR, N_TIPOS };
struct Evento { Tipo tipo; int dato; };
using Handler = void (*)(const Evento&);
std::queue<Evento> cola;
void alPulsar(const Evento& e) { std::printf("pulsador %d\n", e.dato); }
void alMedir(const Evento& e) { std::printf("sensor %d C\n", e.dato); }
void alTick(const Evento& e) {
std::printf("tick %d\n", e.dato);
if (e.dato < 2) cola.push({TICK, e.dato + 1});
}
Handler tabla[N_TIPOS] = { alPulsar, alTick, alMedir };
int main() {
cola.push({TICK, 1}); cola.push({SENSOR, 24}); cola.push({PULSADOR, 4});
while (!cola.empty()) { Evento e = cola.front(); cola.pop(); tabla[e.tipo](e); }
}Cómo leerlo:
- El evento es tipo + dato.
Tipoes unenum, así quePULSADORvale 0,TICK1 ySENSOR2.N_TIPOS(3) sirve para dimensionar la tabla. - La tabla de handlers guarda un puntero a función por tipo, en el mismo orden que el
enum. - El bucle saca el primero (
front+pop) y lo despacha. alTickencola un nuevo tick mientras el dato sea menor que 2.
Antes de mirar la salida en el paso siguiente, haz la traza de la cola en papel.
tabla[SENSOR](e)?- alPulsar
- alTick
- alMedir
- Ninguno: SENSOR no está en la tabla
SENSOR es el índice 2 y tabla[2] es alMedir. Por eso el orden de la tabla tiene que coincidir con el del enum.
La cola empieza con TICK 1, SENSOR 24 y PULSADOR 4. Al atender TICK 1 se encola TICK 2 al final, detrás de los que ya estaban.
Teoría 7. Un event loop completo en una línea
La salida es:
tick 1
sensor 24 C
pulsador 4
tick 2Tres cosas que observar:
- Un handler puede encolar eventos nuevos, que se colocan al final de la cola. El
tick 2creado por el primer handler sale el último: espera a los que ya estaban. - El dispatcher es una línea:
tabla[e.tipo](e). El bucle no conoce la lógica de ningún evento; solo indexa la tabla. - El evento es tipo + dato, y el tipo indexa la tabla.
Compáralo con la barra del bar de la Hora 1: while (comandas.isNotEmpty()) + removeFirst + preparar es exactamente este while (!cola.empty()) + front/pop + tabla[...]. La diferencia es que aquí, en lugar de un when dentro del handler, hay una tabla de despacho que elige el handler por tipo.
tabla[e.tipo](e)tabla[e.tipo](e): indexa la tabla con el tipo del evento y llama al puntero a función obtenido pasándole el evento.
tick 2 sale el último aunque lo creó el primer handler?- Porque los ticks tienen menos prioridad
- Porque se encola al final, detrás de los eventos que ya esperaban
- Porque
alTicklo encola con retraso - Porque el bucle atiende los eventos de dos en dos
std::queue es FIFO: push coloca al final. SENSOR y PULSADOR ya estaban en la cola, así que salen antes que el tick nuevo.
Teoría 8. Prioridades: cuando lo urgente espera
El orden de llegada es justo, pero no distingue una alarma de un registro. Supón cuatro eventos de log de 20 ms cada uno en la cola y que llega una alarma:
| Cola | Orden | La alarma se atiende a los |
|---|---|---|
| Solo FIFO | log 1, log 2, log 3, log 4, ALARMA | 80 ms |
| Con prioridad | ALARMA (prio 9), log 1, log 2, log 3, log 4 (prio 1) | 0 ms |
Cómo funciona
Cada evento lleva un nivel. Se inserta delante del primer evento menos urgente; dentro del mismo nivel, FIFO.
Lo que no hace
No interrumpe el handler que ya corre: adelanta en la cola, no cancela lo que se ejecuta (run-to-completion).
El riesgo
Si lo urgente llega sin parar, lo demás nunca se atiende (inanición). Se evita con límites o con envejecimiento.
El envejecimiento (aging) consiste en ir subiendo la prioridad de un evento cuanto más tiempo lleva esperando, de modo que tarde o temprano adelante a los urgentes. Es lo que le faltaba a la L21 del panel de la caldera.
10500 × 20 ms = 10 000 ms = 10 s. Con prioridad, 0 ms (si no hay ningún handler en curso).
- Envejecimiento: subir la prioridad de lo que lleva mucho esperando
- Poner límites a lo urgente
- Dejar que la alarma interrumpa al handler en curso
- Usar una pila en lugar de una cola
Límites y envejecimiento son los remedios citados. Interrumpir al handler rompería run-to-completion y no resuelve la inanición; una pila empeoraría la justicia del orden.
Teoría 9. Fusión de eventos: coalescencia
Si llegan muchos eventos de estado seguidos, basta con el último. Ejemplo con un mapa que se repinta:
| Cola | |
|---|---|
| Sin fusionar (5 repintados) | mover x=1, mover x=2, tocar marcador, mover x=3, mover x=4 |
| Fusionando (3 eventos, mismo resultado final) | mover x=2, tocar marcador, mover x=4 |
Se funden: eventos de estado
«Dónde está el dedo», «cuánto lleva descargado», «cuánto scroll»: el valor nuevo sustituye al anterior, así que el intermedio no aporta nada.
No se funden: eventos de acción
«Pulsar Comprar», «enviar mensaje»: cada uno cuenta. Fundir dos clics sería perder un pedido.
¿Por qué no fundir mover x=2 con mover x=4 saltándose el «tocar»? Porque cambiaría el orden de los hechos: el marcador se tocó cuando el mapa estaba en x=2, no en x=4.
3scroll 20 sustituye a scroll 10; el clic corta la fusión; scroll 30, 40 y 50 se funden en scroll 50. Quedan 3: scroll 20, clic Comprar, scroll 50.
- Progreso de una descarga (porcentaje)
- Pulsar «Comprar»
- Posición del dedo al arrastrar
- Enviar un mensaje de chat
Progreso y posición son estado: solo importa el último valor. Comprar y enviar son acciones: fundirlas sería perder un pedido o un mensaje.
Teoría 10. Cola llena: elegir qué se pierde y contarlo
Con memoria limitada, el descarte es inevitable. La decisión es qué se tira y que no pase desapercibido.
Ejemplo: una cola de 3 posiciones recibe 5 lecturas seguidas sin que el bucle saque ninguna. Con la política «descartar el más antiguo», las lecturas 1 y 2 son pisadas; en la cola quedan 3, 4 y 5, y el contador de perdidas vale 2.
| Política | Qué descarta | Úsala para | Ejemplo |
|---|---|---|---|
| Descartar el más antiguo | El que lleva más tiempo esperando | Estados: solo importa el último valor | Temperatura, GPS |
| Rechazar el nuevo | El que intenta entrar | Órdenes que no se pueden saltar ni reordenar | Comandos de un protocolo |
| Frenar al productor | Nada: se le hace esperar | No se puede perder nada | Fichero a red (U5) |
Por qué cada una encaja con su ejemplo:
- En una temperatura o un GPS, la lectura vieja ya no vale: mejor quedarse con la nueva.
- En un protocolo de comandos, saltarse una orden intermedia podría dejar al otro extremo en un estado inconsistente: mejor rechazar lo nuevo (y que el emisor lo sepa).
- Al enviar un fichero por la red no se puede perder ni un byte: se frena a quien lee el fichero hasta que haya sitio.
- Descartar el más antiguo
- Rechazar el nuevo
- Frenar al productor
- Vaciar la cola entera
La posición es un estado: solo importa el último valor. Se tira la más antigua.
- Descartar el más antiguo
- Fusionar los comandos iguales
- Rechazar el nuevo
- Descartar uno al azar
Rechazar el nuevo mantiene íntegra la secuencia ya aceptada; el emisor sabe que su comando no entró. Tirar uno antiguo rompería el orden.
4Caben 3 (las lecturas 5, 6 y 7); las 4 primeras se pisan. Contador = 4.
Teoría 11. Run-to-completion
Un handler se ejecuta entero antes de que empiece el siguiente: nada lo interrumpe, ni siquiera un evento urgente.
Lo que ganamos
Los handlers no se pisan entre sí: no necesitan cerrojos para el estado que comparten. El orden de ejecución es predecible y fácil de probar.
Lo que pagamos
Cada evento espera a todo lo que tenga por delante. Un solo handler lento retrasa a todos los demás.
Ya lo has visto dos veces:
- En la barra del bar, el café del desayuno esperó al zumo.
- En el C++, el
tick 2lo creó el primer handler, pero no empezó hasta que acabaron los que ya estaban en la cola.
- Los handlers no necesitan cerrojos para el estado compartido
- Un evento urgente se atiende al instante aunque haya otro handler en marcha
- El orden de ejecución es predecible y fácil de probar
- Un handler lento no afecta a los demás
Sin interrupciones no hay carreras entre handlers, y el orden es reproducible. Pero lo urgente espera y un handler lento sí retrasa a todos: eso es lo que pagamos.
- 15 ms
- 30 ms
- 35 ms
- 40 ms
B espera de 10 a 30 ms (20 ms de latencia), se ejecuta de 30 a 35 ms.
Ejercicio 12. La pantalla que tarda en responder
Una tienda web: una carga de 100 ms en el bucle y, mientras, los clics del usuario van llegando. Cada evento tiene su instante de llegada t y lo que cuesta su handler.
const eventos = [
{ t: 0, nombre: "cargarCatalogo", coste: 100 },
{ t: 16, nombre: "clic Comprar", coste: 2 },
{ t: 32, nombre: "scroll", coste: 2 },
{ t: 48, nombre: "clic Carrito", coste: 2 },
];
let reloj = 0;
for (const e of eventos) {
reloj = Math.max(reloj, e.t);
console.log(`${e.nombre.padEnd(15)} espera ${reloj - e.t} ms`);
reloj += e.coste;
}El razonamiento es el mismo que con el sensor: reloj es cuándo queda libre el bucle; cada evento empieza en max(reloj, t) y espera la diferencia con su llegada.
Haz la traza:
| Evento | Llega (t) | Empieza | Espera |
|---|---|---|---|
| cargarCatalogo | 0 | 0 | 0 |
| clic Comprar | 16 | ? | ? |
| scroll | 32 | ? | ? |
| clic Carrito | 48 | ? | ? |
84Llega en 16 y empieza en 100: espera 84 ms, aunque su handler cuesta solo 2 ms.
56Empieza en 104 (100 + 2 + 2) y llegó en 48: espera 56 ms. El scroll espera 70 (empieza en 102, llegó en 32).
Ejercicio 13. Trocear el trabajo
La salida del ejemplo anterior fue:
cargarCatalogo espera 0 ms
clic Comprar espera 84 ms
scroll espera 70 ms
clic Carrito espera 56 msLos clics cuestan 2 ms, pero esperan 84, 70 y 56 ms porque un handler de 100 ms bloquea el bucle. La solución sin tocar el bucle: trocear la carga en 10 trozos de 10 ms. Cada trozo, al terminar, se vuelve a encolar y los clics se cuelan entre ellos.
const cola = [
{ t: 0, nombre: "cargar", coste: 10, trozos: 10 },
{ t: 16, nombre: "clic Comprar", coste: 2 },
{ t: 32, nombre: "scroll", coste: 2 },
{ t: 48, nombre: "clic Carrito", coste: 2 },
];
let reloj = 0;
while (cola.length) {
const e = cola.shift(); reloj = Math.max(reloj, e.t);
if (!e.trozos) console.log(`${e.nombre.padEnd(13)} espera ${reloj - e.t} ms`);
reloj += e.coste;
if (e.trozos > 1) {
const sig = { ...e, t: reloj, trozos: e.trozos - 1 };
const i = cola.findIndex((x) => x.t > sig.t);
cola.splice(i < 0 ? cola.length : i, 0, sig);
}
}
console.log(`carga terminada en ${reloj} ms`);- Cada trozo termina y devuelve el control: entre dos trozos el bucle atiende otros eventos.
- El siguiente trozo se encola detrás de lo que ya esperaba (se inserta por instante
t). - El coste es mínimo.
clic Comprar espera 4 ms
scroll espera 0 ms
clic Carrito espera 6 ms
carga terminada en 106 ms- Por el coste de reencolar cada trozo
- Por los 6 ms de los tres clics atendidos entre trozos
- Porque cada trozo espera 0,6 ms al bucle
- Porque el último trozo dura 16 ms
100 ms de carga + 3 eventos de 2 ms intercalados = 106 ms. El simulador no cobra nada por reencolar.
- Porque llega en 16, a mitad del segundo trozo (10–20), y tiene que esperar a que termine
- Porque los clics tienen menos prioridad que la carga
- Porque el bucle tarda 4 ms en sacar un evento
- Porque antes se atiende el scroll
El segundo trozo corre de 10 a 20 ms y no se interrumpe: el clic llega en 16 y empieza en 20. Trocear acota la espera al tamaño de un trozo, no la elimina.
Teoría 14. Cómo se nota un handler largo
La misma causa produce síntomas distintos según el sistema:
| Síntoma | Qué pasa por dentro | Dónde se nota |
|---|---|---|
| Latencia | Los eventos esperan en la cola mientras el handler corre | Un toque tarda en reaccionar |
| Pérdida | La cola se llena y se descarta | Lecturas de sensor que faltan |
| Congelación | El bucle no vuelve a mirar la cola | Ventana «no responde», ANR en Android |
| Reinicio | El watchdog no recibe su señal a tiempo | Un firmware que se resetea solo |
Y tiene tres salidas:
Trocear
Hacer un trozo, reencolar el resto y volver (lo que acabas de ver).
Delegar
Pasar el trabajo a otro hilo o tarea y volver con el resultado (U5 y U6).
Acotar y medir
Presupuesto de tiempo por handler y aviso si se supera.
El handler no es el sitio del trabajo lento. Lo que debe hacer es: leer la entrada, actualizar el estado y encolar lo siguiente.
- Latencia
- Pérdida
- Congelación
- Reinicio por watchdog
El handler largo impide que el bucle dé la señal al watchdog a tiempo, y este reinicia el sistema.
- La duración media de los handlers
- La duración del peor handler
- El número de productores
- El tamaño máximo de la cola
Cualquier evento puede caer justo detrás del handler más lento. Por eso la latencia de respuesta la marca el peor handler.
Teoría 15. Android: Looper, Handler y MessageQueue
El hilo principal (UI thread) de una app Android tiene un Looper que saca mensajes de su MessageQueue y se los da a un Handler.
| Pieza genérica | En Android | Detalle |
|---|---|---|
| Evento / mensaje | Message (what, arg1, obj) o Runnable |
Un Runnable es un callback listo para ejecutar |
| Cola | MessageQueue |
Ordenada por hora de entrega (when), no por prioridad |
| Bucle | Looper.loop() |
Un Looper por hilo; la app arranca con el del hilo principal |
| Dispatcher + handler | Handler.dispatchMessage() |
Ligado al Looper de un hilo |
| Fusión / descarte | removeMessages(), postAtFrontOfQueue() |
Se hace a mano, como en la coalescencia |
MessageQueue de Android?- Por prioridad del mensaje
- Por hora de entrega (
when) - Por el hilo que publicó el mensaje
- Es una pila: sale el último publicado
Por when. Un post normal usa when = ahora, y postDelayed lo coloca más adelante en el tiempo.
LooperLooper: hay uno por hilo y Looper.loop() saca mensajes de la MessageQueue y los despacha.
postDelayed(r, 500) y justo después el hilo principal ejecuta un handler de 2 s. ¿Cuándo se ejecuta r?- A los 500 ms exactos, interrumpiendo el handler
- Cuando termine el handler de 2 s, porque
rsolo puede salir con el hilo libre - Nunca: el mensaje caduca
- Inmediatamente, porque postDelayed bloquea 500 ms y luego ejecuta
r queda en la cola con when = ahora + 500, pero el Looper no puede sacarlo mientras el handler de 2 s se ejecuta. Se ejecuta al terminar este, con retraso.
Teoría 16. El presupuesto por fotograma
A 60 Hz hay 16,7 ms entre dos señales vsync para atender los eventos y dibujar. Si un handler tarda más, se pierden fotogramas.
| Pantalla | Presupuesto por fotograma | Qué entra en él |
|---|---|---|
| 60 Hz | 16,7 ms | Eventos de entrada + layout + dibujo |
| 90 Hz | 11,1 ms | Lo mismo, con menos margen |
| 120 Hz | 8,3 ms | Cada handler debe durar pocos ms |
El presupuesto es simplemente 1000 ms / frecuencia: 1000 / 60 ≈ 16,7.
Un onClick de 42 ms
Con vsync cada ~16,7 ms (0, 16, 33, 50, 66 ms…), un onClick lento de 42 ms ocupa el hilo principal durante más de dos fotogramas enteros. Mientras corre, el hilo no puede dibujar, así que se pierden 2 fotogramas y la animación da un tirón.
8,31000 ms / 120 ≈ 8,3 ms para eventos, layout y dibujo.
- Nada, porque es menos de 5 s
- Jank: se pierden fotogramas, aunque no llega a ANR
- Un ANR inmediato
- Un reinicio de la app por watchdog
300 ms supera con mucho los 16,7 ms: se pierden fotogramas y la app «va lenta». No llega a los ~5 s del ANR.
Ejercicio 17. El botón que descarga en el hilo principal
Una app de reparto: al tocar «Actualizar catálogo», la app se queda congelada durante la descarga.
boton.setOnClickListener {
val datos = descargarCatalogo() // 3 s de red
lista.submit(datos)
}- El
onClickcorre en el hilo principal. Durante 3 s no se atiende ningún otro evento. - La lista no se actualiza hasta que acaba. Ni scroll, ni animaciones, ni más toques.
Aplica lo que llevas visto:
- El
onClickes un handler delLooperprincipal. Mientras está dentro dedescargarCatalogo(), el Looper no puede sacar ningún mensaje más de laMessageQueue. - Los toques, el scroll y los vsync siguen llegando… y se acumulan en la cola.
- Con 60 Hz, en 3 s caben unos 180 fotogramas: ninguno se dibuja.
Piensa antes de contestar: ¿qué ve el usuario en esos 3 s? ¿Y si la red tarda 8 s? ¿Dónde debería ejecutarse la descarga y cómo vuelve el resultado a la lista?
- La app funciona normal y la lista aparece a los 3 s
- Una app congelada: no hace scroll, no anima y no responde a los toques
- Un mensaje de error de red
- La lista vacía, pero puede seguir usando la app
El hilo principal está bloqueado dentro del onClick: ni dibuja ni atiende eventos. Todo se queda quieto hasta que la descarga termina.
- Lo mismo, solo que más rato; el sistema no hace nada
- Android cancela la descarga a los 5 s
- Se supera el umbral de ~5 s y el sistema puede mostrar un ANR («la aplicación no responde»)
- La app se reinicia por watchdog
Más de ~5 s sin que el hilo principal responda ⇒ ANR. El usuario puede elegir cerrar la app.
Teoría 18. Trabajo fuera, resultado dentro
La descarga va a otro hilo, y el resultado vuelve al hilo principal publicando en su Handler.
val principal = Handler(Looper.getMainLooper())
boton.setOnClickListener {
thread {
val datos = descargarCatalogo() // 3 s, sin molestar
principal.post { lista.submit(datos) }
}
}- Handler ligado al Looper principal.
Handler(Looper.getMainLooper())es la «puerta de entrada» a la cola del hilo de la UI. - El trabajo lento sale del bucle. El
onClicksolo arranca el hilo: termina en microsegundos. - El resultado se encola.
principal.post { … }mete unRunnableen laMessageQueuedel hilo principal; cuando le toque, el Looper lo ejecuta y la lista se actualiza desde el hilo principal, el único que puede tocar la UI.
Es la salida «delegar» de la tabla de handlers largos. Y fíjate en que el resultado vuelve como un evento más: el patrón productor → cola → bucle → handler aparece de nuevo, con el hilo de descarga como productor.
lista.submit(datos) directamente dentro de thread { }?- Porque sería más lento
- Porque solo el hilo principal puede tocar la UI; hay que encolar el cambio en su Looper
- Porque
threadno puede acceder a variables de fuera - Porque
submitsolo acepta Runnables
Tocar la UI desde otro hilo está prohibido. principal.post { … } convierte la actualización en un mensaje de la cola del hilo principal.
- El
onClicktermina casi al instante - La descarga se ejecuta en el hilo principal pero troceada
- La actualización de la lista se ejecuta en el hilo principal
- Durante la descarga, la app sigue haciendo scroll y atendiendo toques
La descarga no se trocea: se delega a otro hilo. El hilo principal queda libre durante esos 3 s y solo recibe, al final, el Runnable con el resultado.
Teoría 19. Lista de comprobación y puente a la U4
Seis preguntas para diseñar o depurar un sistema por eventos:
| # | Pregunta | Respuesta sana |
|---|---|---|
| 1 | ¿Quién produce y qué hace? | Los productores solo encolan: rápido y sin lógica. |
| 2 | ¿Qué orden y qué prioridad? | FIFO, salvo urgencias; sin inanición. |
| 3 | ¿Qué se fusiona o se descarta? | Solo eventos de estado; y se mide lo perdido. |
| 4 | ¿Cuánto dura el peor handler? | Esa es tu latencia de respuesta. |
| 5 | ¿Dónde va el trabajo largo? | Trocear o delegar; nunca en el bucle. |
| 6 | ¿Cuál es el presupuesto? | 16,7 ms por fotograma, o el plazo del sistema. |
Puente a la Unidad 4
Ya sabemos que los eventos llegan de uno en uno y en orden. Pero un mismo evento («pulsar») debe hacer cosas distintas según el momento: el mismo botón enciende si está apagado y apaga si está encendido; el mismo «SUBIR» del panel puede subir la temperatura o cambiar de menú.
Eso no se resuelve con más colas ni más prioridades: se modela con máquinas de estado, el tema de la U4.
Productor → cola (orden, fusión, descarte) → handler (peor caso, trabajo largo) → presupuesto. Siguen el recorrido de un evento.
- Cómo ejecutar varios handlers a la vez
- Que un mismo evento debe hacer cosas distintas según el momento
- Cómo evitar la inanición
- Cómo descargar datos sin bloquear
La reacción a un evento depende del estado del sistema: eso se modela con máquinas de estado (U4).
Repaso 20. Repaso: bucle de eventos, colas y dispatching
| Concepto | En una frase |
|---|---|
| Ráfaga | Llega mucho de golpe, pero de media da tiempo |
| Sobrecarga | Llega más de lo que se atiende, siempre |
| Inanición | Lo poco urgente no se atiende nunca |
| Jank | Fotogramas perdidos por un handler > 16 ms |
| ANR | Hilo principal bloqueado ~5 s |
- Una cola suficientemente grande lo resuelve
- La cola solo aplaza el problema: hace falta un handler más rápido, paralelismo o frenar al productor
- Basta con poner prioridad a los eventos del sensor
- Con run-to-completion no se acumulan eventos
Es una sobrecarga sostenida: la espera y la cola crecen sin límite. La cola absorbe ráfagas, no sobrecargas.
- Una cola con prioridad interrumpe al handler en curso si llega algo urgente
- Dos eventos «mover» seguidos al final de la cola se pueden fundir
- Dos clics en «Comprar» seguidos se pueden fundir
- Descartar sin contar lo perdido es un fallo escondido
La prioridad adelanta pero no interrumpe; los eventos de acción nunca se funden. Fundir estados y contar los descartes son buenas prácticas.
16,71000 / 60 ≈ 16,7 ms para eventos de entrada, layout y dibujo.
- Llamando a la vista directamente desde el hilo de descarga
- Publicando un Runnable en un
Handlerligado al Looper principal (post) - Usando
postAtFrontOfQueueen el hilo de descarga para que se ejecute ahí - Bloqueando el hilo principal hasta que acabe la descarga
Handler(Looper.getMainLooper()).post { … } encola el cambio en el hilo principal, el único que puede tocar la UI.