Interfaces gráficas en entornos multihilo

ProyectoRadio está implementado completamente sobre la plataforma .NET 2. No es que me guste más o menos que otras, pero como comenté en mi post sobre C, C# es un lenguaje que me parece muy potente y la plataforma permite desarrollar proyectos bastante complejos de una forma asequible.

La naturaleza del software requiere hacer tareas pesadas o largas. Por ejemplo las simulaciones de coberturas, la importación o la exportación de datos, la carga de mapas, etc son tareas que cuando se ejectuan tienen el control del programa durante un tiempo bastante importante.

La naturaleza de las aplicaciones .NET con interfaz gráfica de usuario Windows.Forms es básicamente la misma que la de la API de Windows nativa, solo que muy organizada y bonita. Las aplicaciones son del tipo “orientadas a eventos”. Esto quiere decir que existe un hilo de ejecución único asociado a la aplicación que está siempre dormido. Cuando la aplicación recibe un evento: Click en un botón o desplazamiento de una barra, el sistema le envía un mensaje que la función principal ejectua (todo esto de forma oculta claro) y llama a la función apropiada para manejar ese click.

Lo que el programador escribe son las funciones asociadas a los eventos. Cada control de usuario puede tener unos cuantos eventos. Por ejemplo un control de tipo Button tiene el evento Click. En C# se pueden asociar funciones a eventos de tal forma que si nosotros queremos gestionar el evento de Click solo hay que asociar una función al evento y cada vez que se pulse el botón se llamara a nuestra función. Bien.

Hasta aquí todo parece fácil. El problema veradero viene cuando asociado a ese handler o función asociada a un evento hay una operación muy lenta o costosa. Ejemplos un poco más arriba. Si no se hace nada y sin más se ejecuta la operación, el hilo de ejecución, que es único está entretenido operando y haciendo cuentas costosas y por tanto no retorna del evento, y por supuesto no puede atender más eventos del sistema, por mucho que estos lleguen. El resultado es bastante conocido, la interfaz de usuario (UI) se queda “como bloqueada” y si movemos la ventana fuera del escritorio y la traemos de nuevo se queda blanca. Normal, no se está gestionando el evento de repintado🙂.

Esto me ha traído bastantes problemillas. A primera vista la solución parece fácil. Usar un hilo de ejecución paralelo para ejecutar la tarea pesada. La idea es tan sencilla como demoníaca. Esto es fácil en C# y .NET. Hay una clase asociada a un hilo o thread de ejecución. Sin más que instanciar uno de estos objetos, pasarle una función de ejecución y lanzarla ya tenemos un hilo ejecutándose en paralelo al hilo principal. Bueno, pues bien, ahora nuestro hilo principal, después de lanzar el hilo pesado retorna del gestor del evento y la interfaz sigue respondiendo como es debido. Claro.

Ójala fuera todo así de fácil. Si estamos familiarizados con los sistemas paralelos y la programación concurrente rápidamente vamos a ver un problemilla: Hay que garantizar la coherencia de los datos. Esto quiere decir básicamente que todos los datos que necesite el hilo pesado ejecutándose en background no se modifiquen, o lo hagan de forma coherente, por el hilo principal de ejecución. Esto implica que cualquier pulsación de botón, desplazamiento, etc no modifique los datos importantes. Formas de garantizar esto existen, claro. Una de ellas es sin más haciendo una copia de los datos (shadow) y que estos sean los usados por el hilo de background. No creo que sea la ideal. Tenemos otras como usar mecanismos de interlocking. Ejemplos hay variados: Mutexes, CriticalRegions, Semaphores… lo que sea para bloquear al hilo principal de modificar los datos del hilo pesado. Hay que tener de nuevo cuidado de no bloquear la UI en uno de estos mecanismos.

Además de estos problemas evidentes y muy conocidos, la UI presenta otros problemillas cuando se usa en un entorno multihilo. Solo se puede acceder o modificar los valores de los controles, incluyendo su repintado, desde el hilo donde se crearon. Así, si se llama al constructor de un boton en el hilo principal, que será lo común, cualquier acceso que se haga desde el hilo secundario a este botón: Ver su estado, deshabilitarlo, etc… lanzará una excepción muy bonita y nos obligará a salir de la aplicación con toda probabilidad.

Me imagino que es una forma de proteger la implementación de los controles sin necesidad de complicados mecanismos de interbloqueo pero es una putada. Si te das cuenta que desde el hilo de la tarea pesada es necesario modificar o acceder a la UI, para dar un poquito de feedback o para actualizar una vista, pues cuidadín.

Alternativas a este problema hay muchas, pero principalmente dos: Una alternativa sencilla para problemas sencillos es usar un BackgroundWorker. Este tipo de objeto es una careta a un thread e implementa funciones útiles. Está pensado para el problema que yo planteo así que una vez arrancado el hilo principal retorna y la interfaz sigue funcionando. Cuando desde el hilo pesado hay que dar feedback se llama a una función del objeto BackgroundWorker (GiveFeedback()) y un evento se lanza pero en el hilo de ejecución que creó al BackgroundWorker. Problema resuelto, el feedback se puede dar en el hilo correcto sin más problemas. Lo mismo pasa para cancelar o para terminar el hilo.

El problema a esta primera solución aparece cuando los objetos que se gestionan en el hilo pesado son más complicados y el feedback no es tan sencillo y requiere modificar múltiples vistas. Otro problema es cuando los objetos modificados en el hilo secundario tienen sus propios eventos que pueden ser gestionados por parte de la UI. La solución a este problema puede ser bastante sencilla si se piensa desde un principio.

Allí donde la UI esté asociada a un evento de un objeto y que provoque un cambio o acceso a la UI es necesario comprobar el contexto de la llamada. Todos los controles, bueno realmente todos los objetos heredados de Component implementan una funcionalidad para evitar el problema de los hilos inapropiados. La propiedad InvokeRequired indica si es necesario hacer malabarimos para actualizar la UI. Retorna false en caso de que el hilo es el correcto y true en el caso que el hilo desde el que se llama sea otro diferente a la de creación. De nuevo los controles tienen una función, bueno una pequeña colección de ellas, que permite invocar la ejecución de una función en el hilo correcto. BeginInvoke() e Invoke() son las más importantes. Ambas funciones van a lanzar la ejecución de la función que le indiquemos como parámetro pero en el hilo correcto.

Bufff, esto es todo sobre UI y multithreading que no es poco. La verdad es que puede dar verdaderos dolores de cabeza sin no se diseña desde el principio con cuidado.

2 comentarios to “Interfaces gráficas en entornos multihilo”

  1. Tráfico de un blog « ProyectoRadio (y otras cosas…) Says:

    […] Desde el principio he escrito sobre los temas más variados, desde reflexiones personales hasta recetas, pasando por el desarrollo de software. Pero desde hace ya tiempo la entrada del blog que tiene más lecturas al día es la del pequeño tutorial sobre interfaces en entornos multihilo. Me parece bastante curioso, por ejemplo este post ha tenido la friolera de 7visitas mientras que el segundo clasificado, el dedicado a P.Tinto, ha tenido solo 3, más de un 100% extra para el tutorialillo. […]

  2. Luis Says:

    Por favor, podrias enviarme tu correo para comentar sobre un proyecto de multihilos?

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s


A %d blogueros les gusta esto: