¿Cuándo se sobrecarga la GUI?

Supongamos que eres permanentemente invocar un método de forma asíncrona en el subproceso/despachador de la interfaz de usuario con

while (true) {

    uiDispatcher.BeginInvoke(new Action<int, T>(insert_), DispatcherPriority.Normal, new object[] { });
}

En cada ejecución del programa, observa que la GUI de la aplicación comienza a congelarse después de unos 90 segundos debido a la inundación de invocaciones (el tiempo varía pero se encuentra aproximadamente entre 1 y 2 minutos).

¿Cómo se podría determinar exactamente (¿medir?) el punto en que esto sobrecarga ocurre para detenerlo lo suficientemente temprano?

Apéndice I:

En mi programa real no tengo un bucle infinito. Tengo un algoritmo que itera varios cientos de veces antes de terminar. En cada iteración, agrego una cadena a un control de Lista en mi aplicación WPF. Usé la construcción while (true) { ... } porque coincide mejor con lo que sucede. De hecho, el algoritmo finaliza correctamente y todas las (cientos) cadenas se agregan correctamente a mi Lista, pero después de un tiempo pierdo la capacidad de usar mi GUI hasta que el algoritmo finaliza; luego, la GUI vuelve a responder.

Apéndice II:

El propósito de mi programa es observar un algoritmo particular mientras se está ejecutando. Las cadenas que estoy agregando son entradas de registro: una cadena de registro por iteración. La razón por la que estoy invocando estas operaciones de adición es que el algoritmo se está ejecutando en otro subproceso que no sea el subproceso de la interfaz de usuario. Para ponerme al día con el hecho de que no puedo manipular la interfaz de usuario desde ningún hilo que no sea el hilo de la interfaz de usuario, construí algún tipo de ThreadSafeObservableCollection (pero estoy bastante seguro de que no vale la pena publicar este código porque restaría valor al problema real, ¿qué Creo que la interfaz de usuario no puede manejar la invocación repetida y rápida de métodos.

preguntado el 03 de julio de 12 a las 23:07

Su uso de cursiva is distraer. -

Pruebe DispatcherPriority.Input en su lugar. -

@mindandmedia muchas gracias por su enlace, pero estoy trabajando directamente con el subproceso de la interfaz de usuario, por lo que no creo que los ThreadPools sean de ningún beneficio para mí, ya que se pueden usar para hacer un trabajo en segundo plano: el trabajo que quiero que haga el subproceso de la interfaz de usuario. hacerse -

"[...] para detenerlo lo suficientemente pronto": ¿Por qué se requiere este ciclo infinito? Parece ser que la mejor solución sería no medir cuándo detenerse, sino nunca comenzar en absoluto... Es decir: intenta deshacerte de esto por completo si puedes. -

@stakx Consulte el Apéndice que acabo de agregar a mi publicación. -

7 Respuestas

Es bastante sencillo: lo estás haciendo mal cuando sobrecargas los ojos del usuario. Lo que sucede bastante rápido en lo que respecta a los núcleos de CPU modernos, más allá de las 20 actualizaciones por segundo, la información mostrada comienza a verse borrosa. Algo que aprovecha el cine, las películas se reproducen a 24 fotogramas por segundo.

Actualizar más rápido que eso es solo una pérdida de recursos. Todavía le queda una enorme cantidad de espacio para respirar antes de que el hilo de la interfaz de usuario comience a ceder. Depende de la cantidad de trabajo que le pidas que haga, pero lo típico es un margen de seguridad de x50. Un temporizador simple basado en Environment.TickCount hará el trabajo, activará una actualización cuando la diferencia sea >= 45 mseg.

Respondido 04 Jul 12, 00:07

Muchas gracias por su respuesta. Está señalando el mismo problema que @HenkHoltermann en los comentarios anteriores. Lo que está diciendo me parece absolutamente razonable y actualmente parece que no tengo otra opción en cuanto a dirigirme también hacia su enfoque de reducir los datos que quiero mostrar. Para darme cuenta de que primero tengo que pensar en redefinir mis requisitos, porque creo que no puedo cumplirlos con su enfoque. De todos modos, gracias de nuevo por su ayuda, es muy apreciada. - Marc Wellman

Depende del tipo de contenido. Si el contenido es gráfico e incluye cambios rápidos, incluso 24 fotogramas por segundo pueden no ser suficientes. - Danny Varod

Publicar eso a menudo en la interfaz de usuario es una señal de alerta. Aquí hay una alternativa: poner nuevas cadenas en un ConcurrentQueue y haga que un temporizador los extraiga cada 100 ms.

Muy sencillo y fácil de implementar, y el resultado es perfecto.

Respondido 04 Jul 12, 00:07

hola y gracias De alguna manera, eso es exactamente lo que hice. En mi publicación anterior hace unos días, puede ver más código, incluidas mis colas de retraso. aquí - Marc Wellman

También respondí a tu otra pregunta. Creo que será mejor que elimine todas sus colas, bloqueos y monitores implementados manualmente y simplemente use el ConcurrentQueue incorporado. Una cola por control de interfaz de usuario y subproceso, y un temporizador por cola. - usr

No he usado WPF, solo Windows Forms, pero sugeriría que si hay un control de solo vista que deberá actualizarse de forma asíncrona, la forma correcta de hacerlo es escribir el control para que sus propiedades puedan ser acceder libremente desde cualquier subproceso, y la actualización de un control BeginInvoke la rutina de actualización solo si no hay una actualización pendiente; esta última determinación se puede hacer con un Int32 "bandera" y Interlock.Exchange (el dueño de la propiedad llama Interlocked.Exchange en la bandera después de cambiar el campo subyacente; si la bandera hubiera estado clara, hace un BeginInvoke en la rutina de actualización; la rutina de actualización luego borra el indicador y realiza la actualización). En algunos casos, el patrón se puede mejorar aún más haciendo que la rutina de actualización del control verifique cuánto tiempo ha transcurrido desde la última vez que se ejecutó y, si la respuesta es menos de 20 ms, use un temporizador para activar una actualización 20 ms después de la última vez. el anterior.

Aunque .net puede soportar tener muchos BeginInvoke acciones publicadas en el subproceso de la interfaz de usuario, a menudo no tiene sentido tener más de una actualización para un solo control pendiente a la vez. Limite las acciones pendientes a una (o como máximo un pequeño número) por control, y no habrá peligro de que la cola se desborde.

Respondido 04 Jul 12, 00:07

gracias por su respuesta detallada. > [..] escribir el control para que se pueda acceder libremente a sus propiedades desde cualquier hilo [..]. ¿Cómo podría lograrse eso? ¿No necesito al menos un hilo desde el cual opero en el control? - Marc Wellman

ok, perdón por el mal enlace antes en los comentarios, pero seguí leyendo y tal vez esto sea de ayuda:

The DispatcherOperation object returned by BeginInvoke can be used in several ways to interact with the specified delegate, such as:

Changing the DispatcherPriority of the delegate as it is pending execution in the event queue.
Removing the delegate from the event queue.
Waiting for the delegate to return.
Obtaining the value that the delegate returns after it is executed.

If multiple BeginInvoke calls are made at the same DispatcherPriority, they will be executed in the order the calls were made.

If BeginInvoke is called on a Dispatcher which has shut down, the status property of the returned DispatcherOperation is set to Aborted.

Tal vez puedas hacer algo con la cantidad de delegados que estás esperando...

Respondido 04 Jul 12, 00:07

ahora que leí su apéndice, estoy bastante seguro de que hay una manera más eficiente de lograr eso... - mente y medios

bueno primero, muchas gracias por tus esfuerzos, son muy apreciados. Por favor, sea tan amable y consulte mi segundo Apéndice. - Marc Wellman

Para poner la solución de supercat de una manera más parecida a WPF, intente con un patrón MVVM y luego puede tener un patrón separado ver modelo clase que puede compartir entre subprocesos, tal vez eliminar bloqueos en puntos apropiados o usar la clase de colecciones concurrentes. Implementa una interfaz (creo que es INotifyPropertyChanged y activa un evento para decir que la colección ha cambiado. Este evento debe activarse desde el subproceso de la interfaz de usuario, pero solo necesita

Respondido 04 Jul 12, 00:07

Gracias por su respuesta. Estoy usando el patrón MVVM y he creado una ThreadSaveObservableColection especializada que actúa como parte de mi ViewModel. Aunque no está completo, puede consultar una de mis publicaciones anteriores sobre este tema. aquí - Marc Wellman

Sí, el problema es que su implementación de una colección observable no es útil de ninguna manera. El problema con esto es que simplemente lo basa en la colección observable estándar y llama a la inserción en el subproceso de la interfaz de usuario. Lo que debe hacer es poner en cola las invocaciones de inserción y hacerlo en masa de una sola vez cada 400 ms aproximadamente. - Forbes Lindesay

Después de revisar las respuestas proporcionadas por otros y sus comentarios sobre ellas, su intención real parece ser garantizar que la interfaz de usuario siga respondiendo. Para ello creo que ya habéis recibido buenas propuestas.

Pero aún así, para responder a su pregunta (cómo detectar y marcar la sobrecarga del subproceso de la interfaz de usuario) palabra por palabra, puedo sugerir lo siguiente:

  1. Primero determine cuál debería ser la definición de 'sobrecarga' (por ejemplo, puedo suponer que es 'el subproceso de interfaz de usuario deja de representar los controles y deja de procesar la entrada del usuario' durante un tiempo lo suficientemente grande)
  2. Defina esta duración (por ejemplo, si el subproceso de la interfaz de usuario continúa procesando el procesamiento y los mensajes de entrada en un máximo de 40 ms, diré que no está sobrecargado).
  3. Ahora inicie un DespachadorTemporizador con DispatcherPriority establecido de acuerdo con su definición de sobrecarga (para mi, por ejemplo, puede ser DispatcherPriority.Input o inferior) e Interval suficientemente menor que su 'duración' para la sobrecarga
  4. Mantenga una variable compartida de tipo DateTime y en cada marca del temporizador cambie su valor a DateTime.Now.
  5. En el delegado que pasa a BeginInvoke, puede calcular una diferencia entre la hora actual y la última vez que se activó Tick. Si excede su 'medida' de sobrecarga, entonces el subproceso de la interfaz de usuario está 'sobrecargado' de acuerdo con su definición. A continuación, puede establecer un indicador compartido que se puede comprobar desde el interior de su bucle para tomar las medidas adecuadas.

Aunque lo admito, no es infalible, pero al ajustar empíricamente su 'medida', debería poder detectar la sobrecarga antes de que lo afecte.

Respondido 04 Jul 12, 07:07

Utilizar Cronógrafo para medir las duraciones de actualización mínima, máxima, media, primera y última. (Puede enviar esto a su interfaz de usuario).

Su frecuencia de actualización debe ser < de 1/(la duración promedio de actualización).

Cambie la implementación de su algoritmo para que las iteraciones sean invocadas por un temporizador multimedia, por ejemplo este contenedor .NET or este contenedor .NET. Cuando el temporizador está activado, utilice Enclavado para evitar ejecutar una nueva iteración antes de que se complete la iteración actual. Si necesita iteraciones en el principal, use un despachador. Puede ejecutar más de 1 iteración por evento de temporizador, use un parámetro para esto y, junto con las medidas de tiempo, determine cuántas interacciones ejecutar por evento de temporizador y con qué frecuencia desea los eventos de temporizador.

No recomiendo usar menos de 5 ms para el temporizador, ya que los eventos del temporizador sofocarán la CPU.

Como escribí antes en mi comentario, use DispatcherPriority.Input al enviar al subproceso principal, de esa manera el tiempo de CPU de la interfaz de usuario no se ve sofocado por los envíos. Esta es la misma prioridad que tienen los mensajes de la interfaz de usuario, por lo que no se ignoran.

Respondido 04 Jul 12, 11:07

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.