viernes, 17 de octubre de 2008

vs2008: CheckForIllegalCrossThreadCalls

Introducción

El mundo de la programación es más o menos sencillo hasta que nos metemos en berengenales como el multi-hilo. Aquí la cosa se complica ligeramente, y si tratamos de seguir programando como si fuese programación mono-hilo, antes o después tenemos un error del depurador que viene a decir que un hilo está tratando de tocar datos que tiene otro hilo.


La Chapuza

Además, nos da una solución rápida en plan "que no se note", como es cambiar la propiedad CheckForIllegalCrossThreadCalls a false. En la edad media se cortaba una mano por cosas mucho más leves que esta. Eso es como si está sonando la alarma de incencido y le desactivamos el timbre para que no moleste. ¿No es mas razonable -además de otras muchas cosas- solucionar la causa?


La Solución Elegante

Por fin he entendido cómo hay que hacer las llamadas asíncronas a procesos síncronos en Windows Forms y los famosos métodos Delegados, así que voy a intentar explicarlo por aquí, aun a riesgo de meter la pata -vaya por delante que según la documentación, esta forma que voy a poner no termina de ser la mejor posible-.

En un escenario ideal, creamos un formulario que tiene unos componentes y unos métodos, y éstos cambian el estado de aquellos a discrección. Esto es lo habitual cuando un proceso va poniendo en pantalla información sobre lo que va haciendo, y no es ningún problema cuando se trata de una aplicación mono-hilo, de esas que van haciendo cosas una detrás de otra -en la mayoría de los casos dejando el interface frito- hasta que terminan.


Ponemos algo en un TextBox
de la forma tradiccional


Sin embargo, más tarde o más temprano se nos ocurrirá la feliz idea de evitar el famoso "no responde" en el Administrador de Tareas, e incluso -y esto ya es para nota- el botón de cancelar que de verdad cancele el proceso en ejecución. Y para hacer esto no hay nada que nos complique más la vida que lanzar ese proceso pesado en otro hilo distinto al utilizado por el interface.


El ojo hábil habrá notado que estoy llamando a otro método,
pero a estas alturas podría seguir siendo PonTexto.


Y justo aquí es donde llegamos a nuestro problema, ya que cuando ese subproceso trate de comunicarse con el proceso que lo ha creado -por ejemplo para indicar el estado en el que está en un TextBox-, tendremos esa excepción que dice que:


Operación no válida a través de subprocesos: Se tuvo acceso al
control 'textBox1' desde un subproceso distinto a aquel en que lo creó.


La solución a esto recuerda a la recursividad pero cambiándole el nombre -Microsoft, si no le cambia el nombre a las cosas, no puede decir que lo ha inventado-. La cuestión está en que si cuando vamos a modificar el control detectamos que no estamos en el hilo "padre" lo que hacemos es llamar al mismo método pero del hilo correcto -es decir, el hilo que creó el subproceso en el que estamos-. Más o menos.

La forma en la que sabemos si estamos en el hilo correcto es tan sencillo como consultar la propiedad InvokeRequired del control. Esta nos dirá si podemos modificar el control, o si por el contrario tenemos que "invocar" al método que lo creó. Realmente, la complicación está en realizar esta "invocación", ya que desde mi punto de vista no es nada intuitiva.

[Actualización 22/10/2008: Los dos siguientes párrafos, lejos de ser correctos, tienen errores importantes respecto a lo que es un delegado -la ignorancia es atrevida-. No obstante, los dejo tal y como los publiqué originalmente a la espera de escribir una entrada en la que hable de los mismos]

En primer lugar, para llamar a Invoke necesitamos un delegado del método que vamos a utilizar. ¿Y qué es un Delegado? Se trata de un método con la misma firma que el método a delegar. ¿Y qué significa tener la misma firma? Pues viene a significar que tiene el mísmo número de parámetros, y que éstos son del mismo tipo, incluido el tipo devuelto por el método ¿Por qué? Porque si.

En nuestro caso, la definición -olvidaba decir que un delegado no tiene implementación- del método delegado viene a ser algo sencillo como esto:



Y por fin, para hacer esta llamada segura, lo que nos queda es definir una variable de este delegado y llamar al proceso Invoke. He creado un proceso nuevo llamado PonTextoSeguro para mantener las dos formas de hacerlo. Así, esta es la implementación del nuevo método:



Resumen

De esta forma, lo que hacemos es:

1º La aplicación crea un hilo para ejecutar un proceso pesado.
2º El proceso manda información a la aplicación indicando dónde va (por ejemplo un texto a mostrar en textBox1).
3º El método que va a escribir en textBox1 detecta que está en un subproceso, así que se llama a sí mísmo pero en el proceso de la aplicación.
4º Después de la llamada, cuando ya si estamos en el proceso correcto, escribimos en textBox1.

Así hacemos llamadas asíncronas de una forma sencilla y sin chapuzas innecesarias. Naturalmente el ejemplo es lo más sencillo que se puede imaginar. La cosa se complica un poco cuando el método (PonTextoSeguro) recibe parámetros, pero sólo un poco.

No hay comentarios: