Qué es realmente «No responde» — Cómo decide Windows que una aplicación se ha colgado, y cómo diseñar aplicaciones que no lo hagan
· Go Komura · Windows, Desarrollo en Windows, Investigación de fallos, Multithreading, WinForms, WPF, Win32 API, Diseño de IU
«La aplicación se pone blanca a mitad de una operación y muestra (No responde).» «Recibimos tickets de que se cuelga de vez en cuando, pero nunca se reproduce en una máquina de desarrollo.» — Para las aplicaciones empresariales de Windows, este «No responde» es una de las quejas más habituales. Y lo que resulta sorprendentemente poco conocido es que quien pone la visualización de «No responde» no es la propia aplicación colgada — es el sistema operativo.
¿Cómo sabe Windows que una aplicación se ha «colgado»? ¿Qué es esa ventana blanca esmerilada? Dirigido a desarrolladores que escriben aplicaciones empresariales en Windows y a personal de TI que recibe tickets sobre aplicaciones que se cuelgan, este artículo recorre el juicio de «No responde» desde los fundamentos del bucle de mensajes, y organiza las causas clásicas de los cuelgues, los diseños que no se cuelgan y un procedimiento para investigar el momento del cuelgue — todo anclado en fuentes primarias.
1. Conclusión principal
- «No responde» es el juicio del sistema operativo. Cuando una ventana (y el hilo de GUI que la posee) no está esperando entrada, no está en su secuencia de arranque y no ha recuperado un mensaje (
PeekMessage) durante 5 segundos, el sistema operativo la trata como que no responde. El juicio no es por proceso.1 - La ventana blanqueada es una «ventana fantasma». El sistema operativo ha ocultado la ventana original y ha intercambiado una falsificación de la misma posición, tamaño y aspecto. Todo lo que puede hacer es moverla, minimizarla o cerrarla; el contenido no se está ejecutando. No se crea una ventana fantasma mientras hay un depurador acoplado.2
- La causa de un cuelgue casi siempre se reduce a una sola cosa. El hilo de IU que debería estar bombeando el bucle de mensajes está bloqueado en un trabajo pesado o en una espera. La E/S síncrona, las llamadas de red, las esperas de bloqueo y
SendMessageentre hilos son los clásicos.3 - El principio de diseño es «no espere ni calcule en el hilo de IU». Mueva el trabajo pesado a un hilo trabajador (
async/await+Task.Runen C#) y deje el hilo de IU dedicado a pintar, al progreso y a aceptar la cancelación.4 DoEventsy bombear el bucle de mensajes a mano son un criadero de errores de reentrada. La visualización de «No responde» desaparece, pero la estructura ahora deja que eventos arbitrarios interrumpan a mitad del trabajo. La separación, no la evasión, es el enfoque adecuado.- La investigación empieza capturando el estado en el momento del cuelgue. Tome un volcado y mire la pila del hilo de IU, y casi siempre puede identificar en qué está esperando.
2. Premisa: las aplicaciones de Windows las impulsan los mensajes
Para entender «No responde», primero necesita asumir que una aplicación GUI de Windows está dirigida por eventos. Una aplicación GUI no va a buscar la entrada por sí misma; recibe mensajes que entrega el sistema operativo (ratón, teclado, peticiones de repintado, temporizadores, etc.) y actúa sobre ellos.3
Cada hilo que crea una ventana tiene una cola de mensajes y ejecuta un bucle de mensajes como este.
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // the window procedure is called
}
GetMessage recupera un mensaje de la cola, y DispatchMessage llama al procedimiento de ventana de esa ventana (la función de manejo de mensajes). El manejo del clic de un botón, el repintado y los manejadores de eventos de WinForms o WPF, en el fondo, se ejecutan todos dentro de una iteración de este bucle.5
flowchart TB
accTitle: Estructura básica del bucle de mensajes
accDescr: El sistema operativo coloca la entrada de ratón, teclado y demás en la cola de mensajes del hilo; el bucle del hilo de IU la recupera con GetMessage, llama al procedimiento de ventana con DispatchMessage y vuelve al principio del bucle cuando termina el procesamiento
os["SO (entrada, peticiones de repintado, temporizadores)"] --> q["Cola de mensajes del hilo"]
q --> gm["Recuperar con GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["Manejar en el procedimiento de ventana"]
wp --> gm
Figura 1: El corazón de una aplicación GUI es el bucle de mensajes; cada manejador de eventos se ejecuta como una iteración de este bucle.
Esta estructura tiene una consecuencia importante. Si hace un trabajo que consume tiempo dentro del procedimiento de ventana (un manejador de eventos), el bucle no puede recuperar el siguiente mensaje mientras tanto. No puede reaccionar ni a clics ni a peticiones de repintado — eso es lo que un «cuelgue» es realmente.
También conviene asumir que los mensajes se entregan por dos caminos. PostMessage coloca el mensaje en la cola y regresa de inmediato, y el bucle recupera y procesa los mensajes en orden. SendMessage, en cambio, llama al procedimiento de ventana de forma directa y no regresa al llamador hasta que el procesamiento termina.36 Esa diferencia se lleva tal cual a la discusión de interbloqueos del capítulo 4.
flowchart TB
accTitle: Dos caminos de entrega de mensajes
accDescr: PostMessage coloca el mensaje en la cola y regresa de inmediato; el bucle de mensajes recupera y procesa los mensajes en orden. SendMessage llama al procedimiento de ventana de forma directa y no regresa al llamador hasta que el procesamiento termina
pm["PostMessage"] --> q2["Colocar en la cola (regresa de inmediato)"]
q2 --> loop["El bucle recupera y procesa en orden"]
sm["SendMessage"] --> direct["Llamar al procedimiento de forma directa"]
direct --> w2["No regresa hasta que el procesamiento termina"]
Figura 2: Aunque ambos «envían un mensaje», un Post encolado y un Send que espera la finalización tienen naturalezas completamente distintas.
3. Cómo se juzga «No responde» — La regla de los 5 segundos y la ventana fantasma
Entonces, ¿cómo sabe el sistema operativo que «esta aplicación se ha colgado»? El criterio está documentado de forma oficial. El sistema operativo trata una ventana como que no responde cuando no está esperando entrada, no está en su secuencia de arranque y no ha llamado a PeekMessage (recuperación de mensajes) durante 5 segundos.1 En otras palabras, el sistema operativo vigila si «el bucle de mensajes está girando de verdad» como se tomaría un pulso, y si no hay pulso durante 5 segundos juzga que la ventana no responde (la documentación afirma que este valor de 5 segundos puede cambiar en el futuro). La unidad de juicio es la ventana y el hilo de GUI que la posee; en una aplicación con varios hilos de IU, que un hilo se cuelgue no significa que las ventanas de otro hilo estén muertas. El hilo que hay que mirar en un volcado es el propietario de la ventana colgada.
Lo que ocurre con una ventana de nivel superior que ha sido juzgada también está documentado. El sistema operativo oculta la ventana original y la sustituye por una «ventana fantasma» que tiene el mismo orden Z, posición, tamaño y aspecto. Todo lo que el usuario puede hacer con ella es moverla, cambiar su tamaño o (de forma forzosa) cerrarla. La aplicación de dentro no está respondiendo de verdad, así que ninguna otra operación funciona.2
flowchart TB
accTitle: Juicio de ventana colgada e intercambio por ventana fantasma
accDescr: Cuando el hilo de IU está bloqueado en un trabajo pesado y la recuperación de mensajes se detiene durante 5 segundos, el sistema operativo juzga que la ventana no responde, oculta la original, intercambia una ventana fantasma del mismo aspecto y ofrece al usuario solo mover, minimizar y cerrar
busy["Hilo de IU bloqueado en un trabajo pesado"] --> stop["Se detiene la recuperación de mensajes"]
stop --> judge{"¿Han transcurrido 5 segundos?"}
judge -->|"no"| stop
judge -->|"sí"| ghost["Intercambiar una ventana fantasma"]
ghost --> u1["El título muestra (No responde)"]
ghost --> u2["Blanco esmerilado; solo mover y cerrar"]
Figura 3: Tanto el texto «No responde» como la pantalla blanca pertenecen a la ventana fantasma que intercambió el sistema operativo, no a la aplicación colgada.
La cadena «(No responde)» que aparece en la barra de título, y el aspecto blanco esmerilado bajo el tema Aero, pertenecen ambos a esta ventana fantasma. Se siguen dos consecuencias prácticas.
- Para cuando se muestra «No responde», el hilo que posee esa ventana no ha estado procesando mensajes durante al menos 5 segundos. No es que «la visualización apareció demasiado pronto» — el hilo de IU está bloqueado con certeza.
- No se crea una ventana fantasma mientras hay un depurador acoplado.2 Cuando parece que «bajo el depurador nunca pasa a No responde, pero en la versión de publicación sí», el cuelgue en sí puede ser el mismo y solo diferir la visualización.
También hay una API, DisableProcessWindowsGhosting, que deshabilita este intercambio para todo el proceso.7 Está pensada para casos especiales como terminales de quiosco en los que no quiere que el sistema operativo haga que una ventana parezca operable por su cuenta. Llamarla impide que aparezca la visualización de «No responde», pero el hecho de que la aplicación está colgada no cambia. Entienda que esto no es algo que una aplicación general use como contramedida de «No responde».
4. Por qué se cuelgan las aplicaciones — Patrones clásicos que bloquean el hilo de IU
La causa, reducida, es un solo punto — «el hilo de IU no vuelve al bucle de mensajes» — pero las formas que se encuentra en la práctica caen en unos pocos clásicos.
flowchart TB
accTitle: Clasificación de las causas clásicas que bloquean el hilo de IU
accDescr: Las cuatro familias clásicas — E/S síncrona y llamadas de red, esperas de bloqueo, SendMessage entre hilos e implicación de COM STA — se reducen todas al mismo único punto de que el hilo de IU no puede volver al bucle de mensajes
kind{"¿Cuál es la causa clásica?"}
kind --> io{"¿E/S o un bloqueo?"}
kind --> other{"¿SendMessage o COM?"}
io --> c1["E/S síncrona y red"]
io --> c2["Esperas de bloqueo"]
other --> c3["SendMessage"]
c3 -.-> c3n["entre hilos"]
other --> c4["Implicación de COM STA"]
c1 --> core["La IU no puede volver"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["Comprobación de No responde"]
Figura 4: El síntoma visible es el mismo, pero el culpable que bloquea el hilo cae en cuatro familias, y la contramedida difiere para cada una.
E/S síncrona y llamadas de red. Esta es la más habitual. El patrón de hacer de forma síncrona, dentro de un manejador de clic de botón, una lectura o escritura de un archivo grande, una consulta a una base de datos, una llamada a una API web o el acceso a un archivo en una unidad de red. En una máquina de desarrollo termina en una fracción de segundo, así que no se entera; la latencia de red de producción o un tropiezo del servidor de archivos lo convierten en una espera de decenas de segundos, y recibe tickets de que «se cuelga de vez en cuando». Las unidades de red tienen tiempos de espera largos cuando la conexión está caída, y causan que el síntoma empeore de forma dramática.
Esperas de bloqueo. El patrón en el que el hilo de IU intenta tomar un bloqueo sobre datos compartidos con un hilo trabajador, y acaba esperando a un trabajador que sostiene ese bloqueo durante mucho tiempo. La disciplina de bloqueos se cubre en detalle en la serie práctica de multithreading.
SendMessage entre hilos. SendMessage no regresa hasta que el procedimiento de la ventana de destino ha terminado de procesar.6 Cuando lo envía a una ventana de otro hilo, se hace esperar al emisor hasta que ese hilo está en un estado en el que puede procesar mensajes. Si el hilo de destino está él mismo esperando algo, tiene un interbloqueo de mensajes en el que cada lado espera al otro.3 Enviar a HWND_BROADCAST en particular lo arrastrará en cuanto una sola ventana no esté respondiendo. Cuando no puede permitirse esperar, considere SendMessageTimeout o PostMessage, que no espera una respuesta.8
sequenceDiagram
accTitle: Interbloqueo causado por SendMessage entre hilos
accDescr: Si un hilo trabajador envía SendMessage a una ventana del hilo de IU mientras el hilo de IU está bloqueado esperando el resultado del trabajador, cada lado espera a que el otro termine y tiene un interbloqueo
participant U as Hilo de IU
participant W as Hilo trabajador
U->>U: Esperando a que el trabajador termine (bloqueado)
W->>U: SendMessage (no regresa hasta que se procesa)
Note over U: No puede procesar mensajes (bloqueado)
Note over W: No puede regresar de SendMessage
Note over U,W: Esperándose mutuamente — interbloqueo
Figura 5: «El hilo de IU espera al trabajador, y el trabajador espera al hilo de IU a través de SendMessage» es un interbloqueo clásico.
Implicación del apartamento COM. Las llamadas a un objeto STA se entregan como mensajes de ventana, así que cuando el hilo de IU (STA) está bloqueado, las llamadas COM de otros hilos quedan bloqueadas también como daño colateral. Esa estructura se explica en el artículo de STA/MTA de COM.
La acumulación de «es solo un momento». Incluso una llamada síncrona de 50 ms, invocada 100 veces en un bucle, son 5 segundos. El umbral de No responde es de 5 segundos, pero la «lentitud» percibida empieza en torno a los 100 ms. Una regla práctica de diseño es «el hilo de IU solo puede quedar bloqueado durante milisegundos».
5. Diseños que no se cuelgan — Sacar el trabajo pesado del hilo de IU
El principio de diseño es una sola cosa: saque el trabajo que consume tiempo del hilo de IU. En C# (WinForms/WPF), async/await es el enfoque adecuado más corto.
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU-heavy work, or APIs that are only synchronous, go to a worker via Task.Run
var result = await Task.Run(() => HeavyCalculation(input));
// For I/O, use APIs that are natively async (they do not consume a thread either)
var data = await httpClient.GetStringAsync(url);
// After await you are back on the UI thread, so you can touch controls directly
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// An exception leaking from an async void handler will take the app down. Catch it here
MessageBox.Show($"The operation failed: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
Hay tres puntos. Primero, mientras await está esperando, el hilo de IU está de vuelta en el bucle de mensajes, así que no pasa a No responde. Segundo, la continuación después de await regresa al hilo de IU, así que puede tocar los controles con normalidad después (tocar un control de forma directa desde un hilo trabajador está prohibido; si lo necesita, use Control.Invoke / Dispatcher.InvokeAsync).4 Tercero, deshabilite el botón mientras el trabajo se está ejecutando, y por lo demás mate la reentrada por diseño.
El panorama es el mismo en Win32 nativo: entregue el trabajo a un hilo trabajador, notifique al hilo de IU la finalización como un mensaje personalizado mediante PostMessage y actualice la IU en el procedimiento de ventana. PostMessage solo coloca el mensaje en la cola y regresa de inmediato, así que el lado del trabajador tampoco queda bloqueado.6 Escriba la espera del propio hilo trabajador con la disciplina cubierta en el artículo de las variables de condición.
flowchart TB
accTitle: División de papeles en una aplicación que no se cuelga
accDescr: El hilo de IU se encarga solo de aceptar la entrada, mostrar el progreso y aceptar la cancelación; un hilo trabajador ejecuta el trabajo pesado y devuelve la finalización al hilo de IU mediante PostMessage o una continuación de await
ui["Hilo de IU: entrada, progreso, cancelar"] -->|"Entregar el trabajo"| w["Hilo trabajador: trabajo pesado"]
w -->|"PostMessage / continuación de await"| ui
ui -.-> ng["Ni E/S síncrona ni cálculo largo en el hilo de IU"]
Figura 6: Mantenga el hilo de IU como el «mostrador de recepción», entregue siempre el trabajo pesado a un trabajador y tome solo la notificación de finalización.
Lo que quiere evitar es la técnica de insertar Application.DoEvents() o un bucle PeekMessage entre trozos de trabajo pesado solo para mantener viva la visualización. Esquiva No responde, pero manejadores de eventos arbitrarios reentran en medio del trabajo. Un segundo clic del botón, cerrar el formulario durante el procesamiento, el disparo de un temporizador — cualquiera de ellos puede corromper datos que aún se están procesando, y los errores dependen del momento y son difíciles de reproducir. Mantenga el bombeo manual del bucle de mensajes dentro de una estructura limitada, como un cuadro de diálogo modal de progreso, y por regla general resuélvalo con separación.
sequenceDiagram
accTitle: Línea de tiempo de un error de reentrada causado por DoEvents
accDescr: Llamar a DoEvents en medio de un trabajo pesado deja que el manejador de eventos de un clic encolado interrumpa y se ejecute, reescriba datos que aún se están procesando y luego reanude el trabajo original, produciendo una corrupción de datos dependiente del momento
participant U as Hilo de IU
U->>U: Empieza el trabajo pesado (datos en proceso)
U->>U: DoEvents (procesar mensajes encolados)
Note over U: El manejador del reclic del botón interrumpe
U->>U: El trabajo que interrumpe reescribe los datos
U->>U: El trabajo original se reanuda (datos ya incoherentes)
Figura 7: DoEvents borra «No responde» a cambio de invitar eventos arbitrarios al medio del trabajo.
Para un trabajo de larga duración, incluya también visualización de progreso y cancelación en el diseño. Envíe el progreso a la IU con IProgress<T> y comunique la interrupción con un CancellationToken, y el usuario puede ver que «está trabajando» y no llegará a una terminación forzosa (que a menudo es una causa de corrupción de datos).
flowchart TB
accTitle: Flujo de progreso y cancelación para un trabajo de larga duración
accDescr: El hilo trabajador envía el progreso al hilo de IU mediante IProgress; una acción de cancelar en la IU llega al trabajador a través de un CancellationToken; el trabajador se detiene en un límite conveniente y limpia
w3["Trabajador: trabajo de larga duración"] -->|"Progreso mediante IProgress"| ui2["IU: progreso y un botón Detener"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["Detenerse en un límite y limpiar"]
Figura 8: El progreso es «trabajador → IU»; la cancelación es «IU → trabajador». Incluya este canal bidireccional delgado en el diseño desde el principio.
6. Investigar el momento del cuelgue
En una investigación de «se cuelga de vez en cuando», lo más valioso es el estado de los hilos en el momento exacto en que está colgada. Reinicie, y la evidencia se ha ido.
Tome un volcado. En la pestaña Detalles del Administrador de tareas, haga clic con el botón derecho en el proceso de destino → «Crear archivo de volcado». Eso solo le da un volcado completo con la pila de todos los hilos. Solo con decirle al personal de TI que recibe los tickets «cuando se cuelgue, tome esto antes de cerrarlo» cambia mucho la tasa de éxito de la investigación. Para construir un mecanismo de recolección, véase el artículo de recolección de volcados de memoria.
Mire la pila del hilo de IU. Abra el volcado en WinDbg y mire la pila del hilo que está bombeando el bucle de mensajes (normalmente el hilo 0). La E/S síncrona aparece como ReadFile o una API de red, una espera de bloqueo como una llamada de la familia WaitFor…, y SendMessage entre hilos como una espera dentro de SendMessage — tal cual. Cómo leerla se explica en el artículo introductorio de WinDbg.
Mírelo en vivo. Con Process Explorer puede inspeccionar la lista de hilos y las pilas en el momento. Cuando está de forma estable lento, tome una traza WPR y analice las esperas del hilo de IU a lo largo del tiempo (WPR/WPA en la práctica).
flowchart TB
accTitle: Procedimiento básico para investigar No responde
accDescr: Tome un volcado en el momento del cuelgue, mire la pila del hilo de IU, identifique si está detenido en E/S síncrona, una espera de bloqueo o SendMessage entre hilos, y conéctelo a la corrección de diseño correspondiente
hang["El momento del cuelgue"] --> dump["Tomar un volcado (antes de cerrar)"]
dump --> stack["Mirar la pila del hilo de IU"]
stack --> io["E/S síncrona o espera de red"]
stack --> lock["Espera de bloqueo"]
stack --> sm["SendMessage entre hilos"]
io -.-> fix["Separar el sitio a un trabajador"]
lock -.-> fix
sm -.-> fix
Figura 9: La estrella de la investigación es «un volcado del momento del cuelgue»; la pila del hilo de IU es en sí misma la clasificación de la causa.
También puede estandarizar cómo hace un primer corte a partir del síntoma. Si siempre se cuelga en una operación concreta, sospeche primero E/S síncrona dentro de ese manejador. Si se cuelga raramente y sin correlación con una operación, sospeche el orden de bloqueos o un interbloqueo de SendMessage entre hilos, y empareje en el volcado los destinos de espera de ambos hilos. Si se cuelga solo en un entorno concreto, sospeche tiempos de espera por factores ambientales como una unidad de red, un proxy o un software antivirus.
7. Resumen
- «No responde» es un mecanismo en el que el sistema operativo juzga que una aplicación no ha recuperado un mensaje durante 5 segundos e intercambia una ventana fantasma. Quien pone la visualización es el sistema operativo, no la aplicación.
- La causa de un cuelgue es un solo punto: «el hilo de IU no puede volver al bucle de mensajes». La E/S síncrona, la red, las esperas de bloqueo y SendMessage entre hilos son los clásicos.
- La contramedida es sacar el trabajo pesado del hilo de IU. En C#,
async/await+Task.Run; en Win32, un hilo trabajador +PostMessage. Impida la reentrada durante la ejecución por diseño, como deshabilitar el botón. - Evadirlo con
DoEventses a cambio de errores de reentrada.DisableProcessWindowsGhostingsolo quita la visualización. Ninguno de los dos es una corrección de causa raíz. - Para la investigación, un volcado del «momento del cuelgue» es lo más importante. La causa casi siempre está escrita en la pila del hilo de IU tal cual.
Desde el punto de vista del usuario «No responde» es «está roto», pero una vez que conoce el mecanismo puede traducirlo a la frase precisa «el hilo de IU no volvió durante 5 segundos». Trabajando hacia atrás desde esa única frase, las causas candidatas, la corrección y el procedimiento de investigación se caen de forma natural.
Artículos relacionados
- Despertares espurios — Por qué las variables de condición despiertan «sin haber sido notificadas» y cómo esperar correctamente en Windows
- Buenas prácticas de multithreading en la práctica — Edición .NET: qué decidir antes de aumentar los hilos
- Conocimientos básicos de STA/MTA en COM - El modelo de subprocesos y cómo evitar los bloqueos
- WinDbg + SOS para leer volcados de memoria — introducción práctica al análisis tras la recolección
- Process Explorer / Handle / VMMap en la práctica ── cómo rastrear cuelgues, fugas y «archivo en uso» desde el estado actual
- El apagado de Windows visto desde la aplicación ── cómo sobrevivir correctamente a la notificación de cierre, el reinicio y el corte de energía
Áreas de consultoría relacionadas
En KomuraSoft LLC nos encargamos de investigaciones de causa raíz de aplicaciones empresariales que «se cuelgan de vez en cuando» o pasan a «No responde» (análisis de volcados y análisis de trazas), de refactorizar código de IU legado lleno de trabajo síncrono hacia async/await y la separación en hilos trabajadores, y de revisiones de diseños de IU que no se congelan. Incluso cuando aún no tiene un procedimiento de reproducción, podemos ayudar empezando por el diseño de cómo recoger evidencia.
- Investigación de fallos y análisis de causas
- Consultoría técnica y revisión de diseño
- Desarrollo de aplicaciones Windows
- Contacto
Referencias
-
Microsoft Learn, IsHungAppWindow function (winuser.h). Sobre el criterio de juicio de que una aplicación se trata como que no responde cuando «no está esperando entrada, no está en su secuencia de arranque y no ha llamado a PeekMessage durante el tiempo de espera interno de 5 segundos»; sobre que este criterio de 5 segundos está sujeto a cambio; y sobre que la función siempre devuelve TRUE para una ventana fantasma. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). Sobre que el sistema trata una ventana de nivel superior como que no responde cuando deja de responder a los mensajes durante varios segundos y la sustituye por una ventana fantasma del mismo orden Z, posición, tamaño y aspecto; sobre que el usuario solo puede moverla, cambiar su tamaño o cerrarla; y sobre que no se crea una ventana fantasma mientras hay un depurador acoplado. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. Sobre que las aplicaciones de Windows están dirigidas por eventos y el procedimiento de ventana procesa los mensajes; sobre la distinción entre mensajes encolados y mensajes enviados de forma directa; sobre el intercambio de una ventana que no responde por una ventana fantasma; y sobre la sección que cubre el interbloqueo de hilos que se envían mensajes entre sí. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). Sobre que los controles de WinForms no son seguros de tocar desde ningún hilo distinto del que los creó; sobre usar Invoke/BeginInvoke para actualizaciones desde otro hilo; y sobre patrones asíncronos seguros usando async/await o BackgroundWorker. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. Sobre una implementación típica del bucle de mensajes con GetMessage, TranslateMessage y DispatchMessage, y sobre cómo inspeccionar una cola de mensajes. ↩
-
Microsoft Learn, SendMessage function (winuser.h). Sobre que SendMessage llama al procedimiento de ventana de la ventana especificada y no regresa hasta que el procesamiento termina; sobre que un envío a una ventana de otro hilo hace esperar al emisor hasta que ese hilo procesa el mensaje; y sobre la diferencia con PostMessage, que coloca el mensaje en la cola sin esperar una respuesta. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). Sobre poder deshabilitar, para el proceso GUI que llama, la característica de ventana fantasma que hace que una ventana que no responde se pueda minimizar, mover y cerrar; y sobre que la deshabilitación dura durante la vida del proceso. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). Sobre poder enviar un mensaje con un tiempo de espera; y sobre una marca (SMTO_ABORTIFHUNG) que regresa sin esperar cuando la ventana no está respondiendo (ha sido juzgada colgada). ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
DllMain y el bloqueo del cargador — La razón real de que le digan «no haga nada en la inicialización de la DLL»
Por qué no debe llamar a LoadLibrary ni sincronizar con otros hilos desde DllMain. A partir de fuentes primarias, este artículo explica c...
La API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork
¿Dispersa llamadas a CreateThread por todo el código nativo? Este artículo explica la API del grupo de hilos de Win32 rediseñada en Vista...
Aplicaciones que se rompen al reanudar de la suspensión — Cómo funcionan los eventos de energía de Windows y cómo construir aplicaciones empresariales que los sobrevivan
Abrió el portátil y las conexiones de la aplicación empresarial estaban muertas: la causa es un diseño que nunca contempló la suspensión....
Despertares espurios — Por qué las variables de condición despiertan «sin haber sido notificadas» y cómo esperar correctamente en Windows
La espera de una variable de condición puede regresar incluso cuando no ha llegado ninguna notificación (un despertar espurio). Este artí...
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Hilo de UI y temporizadores
Hilo de UI de WPF / WinForms, flujos asíncronos, Dispatcher y diseño de temporizadores.
Investigación de fallos y problemas prolongados
Fallos intermitentes, diagnóstico de comunicaciones, bloqueos prolongados y pruebas de rutas de error.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Bajo qué condiciones aparece «No responde»?
- El sistema operativo juzga una ventana colgada cuando una aplicación con ventana no está esperando entrada, no está en su secuencia de arranque y no ha recuperado un mensaje (PeekMessage) durante 5 segundos. La ventana de nivel superior colgada se oculta y se sustituye por una «ventana fantasma» de la misma posición, tamaño y aspecto. El texto «(No responde)» de la barra de título y el aspecto blanco esmerilado pertenecen a esta ventana fantasma, que solo le deja mover, minimizar o cerrar. En otras palabras, «No responde» no es algo que muestre la propia aplicación — es una pantalla que el sistema operativo pone en nombre de la aplicación.
- ¿Hay un ajuste para que «No responde» no aparezca mientras el trabajo está en curso?
- Llamar a DisableProcessWindowsGhosting deshabilita el intercambio por una ventana fantasma para ese proceso. Eso solo hace el cuelgue menos visible para el usuario, no obstante — la ventana sigue sin reaccionar a la entrada, y desde el punto de vista del usuario es una congelación completa, sin forma de moverla o cerrarla. La corrección real no es suprimir la visualización, sino mover el trabajo pesado a un hilo trabajador para que el hilo de IU no quede bloqueado ni una décima de segundo, y mucho menos cinco. Tenga en cuenta también que el sistema operativo no crea una ventana fantasma mientras hay un depurador acoplado, así que puede parecer que «No responde» no ocurre nunca durante la depuración.
- ¿Es aceptable evitar «No responde» con DoEvents (bombeando el bucle de mensajes a mano)?
- No se recomienda. Hacer girar DoEvents o un bucle PeekMessage en medio de un trabajo pesado esquivará el juicio de ventana colgada, pero entonces cualquier manejador de eventos puede reentrar — un segundo clic del botón, cerrar la ventana, un temporizador, etc. Que otro manejador reescriba datos que aún se están procesando, o toque un formulario que se suponía cerrado y lance, produce errores de reentrada que dependen del momento y son difíciles de reproducir — peores que el propio «No responde». El enfoque adecuado es mover el propio trabajo a un hilo trabajador con Task.Run o similar, y dejar que el hilo de IU se encargue solo de mostrar el progreso y aceptar la cancelación.
- ¿Cómo actualizo la IU (los controles) desde un hilo trabajador?
- Los controles de WinForms y los elementos de WPF solo pueden tocarse desde el hilo que los creó (normalmente el hilo de IU). Tocarlos de forma directa desde un hilo trabajador provoca excepciones o comportamiento indefinido. En C#, async/await es el camino más fácil: la continuación después de await regresa al hilo de IU que llamó, así que puede actualizar los controles con normalidad después del await. Para cambiar de forma explícita, use Control.Invoke/BeginInvoke en WinForms y Dispatcher.InvokeAsync en WPF. En Win32 nativo, el patrón establecido es que el hilo trabajador envíe con PostMessage un mensaje de finalización personalizado al hilo de IU, y que el procedimiento de ventana actualice la IU.
- ¿Cómo investigo por qué una aplicación muestra «No responde»?
- Lo importante es capturar el estado en el «momento mismo» del cuelgue. Primero tome un volcado completo desde la pestaña Detalles del Administrador de tareas con «Crear archivo de volcado» y luego, en WinDbg, mire la pila del hilo de IU (el hilo que ejecuta el bucle de mensajes). Si está atascado en E/S síncrona, una espera de red, una espera de bloqueo o esperando a otro hilo a través de SendMessage, aparece en la pila tal cual. Para mirar un proceso en vivo, la lista de hilos y la vista de pila de Process Explorer son útiles; para seguirlo a lo largo del tiempo, capturar una traza WPR es eficaz. Véase también el artículo introductorio de WinDbg de este sitio, Process Explorer en la práctica y WPR/WPA en la práctica.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.