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

· · 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 SendMessage entre 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.Run en C#) y deje el hilo de IU dedicado a pintar, al progreso y a aceptar la cancelación.4
  • DoEvents y 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

Estructura básica del bucle de mensajesEl 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 procesamientoSO (entrada, peticiones de repintado, temporizadores)Cola de mensajes del hiloRecuperar con GetMessageDispatchMessageManejar en el procedimiento de ventana

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.

Dos caminos de entrega de mensajesPostMessage 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 terminaPostMessageColocar en la cola (regresa de inmediato)El bucle recupera y procesa en ordenSendMessageLlamar al procedimiento de forma directaNo 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

Juicio de ventana colgada e intercambio por ventana fantasmaCuando 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 cerrarnoHilo de IU bloqueado en un trabajo pesadoSe detiene la recuperación de mensajes¿Han transcurrido 5 segundos?Intercambiar una ventana fantasmaEl título muestra (No responde)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.

Clasificación de las causas clásicas que bloquean el hilo de IULas 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¿Cuál es la causa clásica?¿E/S o un bloqueo?¿SendMessage o COM?E/S síncrona y redEsperas de bloqueoSendMessageentre hilosImplicación de COM STALa IU no puede volverComprobació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

Interbloqueo causado por SendMessage entre hilosSi 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 interbloqueoHilo trabajadorHilo de IUHilo trabajadorHilo de IUNo puede procesar mensajes (bloqueado)No puede regresar de SendMessageEsperándose mutuamente — interbloqueoEsperando a que el trabajador termine (bloqueado)SendMessage (no regresa hasta que se procesa)

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.

División de papeles en una aplicación que no se cuelgaEl 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 awaitEntregar el trabajoPostMessage / continuación de awaitHilo de IU: entrada, progreso, cancelarHilo trabajador: trabajo pesadoNi 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.

Línea de tiempo de un error de reentrada causado por DoEventsLlamar 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 momentoHilo de IUHilo de IUEl manejador del reclic del botón interrumpeEmpieza el trabajo pesado (datos en proceso)DoEvents (procesar mensajes encolados)El trabajo que interrumpe reescribe los datosEl 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).

Flujo de progreso y cancelación para un trabajo de larga duraciónEl 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 limpiaProgreso mediante IProgressCancellationTokenTrabajador: trabajo de larga duraciónIU: progreso y un botón DetenerDetenerse 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).

Procedimiento básico para investigar No respondeTome 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 correspondienteEl momento del cuelgueTomar un volcado (antes de cerrar)Mirar la pila del hilo de IUE/S síncrona o espera de redEspera de bloqueoSendMessage entre hilosSeparar el sitio a un trabajador

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 DoEvents es a cambio de errores de reentrada. DisableProcessWindowsGhosting solo 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

Á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.

Referencias

  1. 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

  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

  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

  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

  5. 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. 

  6. 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

  7. 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. 

  8. 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 recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

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.

Volver al blog