Subprocesos Una de las cosas que nos permitió la evolución de 16 bits a 32 bits fue la posibilidad de escribir código que utilizara subprocesos, pero, aunque los desarrolladores de Visual C++ han podido utilizar subprocesos desde hace tiempo, los desarrolladores de Visual Basic no han tenido realmente un modo fiable para hacerlo, al menos hasta ahora. Las técnicas anteriores implicaban el acceso a la funcionalidad de subprocesos disponibles para los desarrolladores Visual C++. Aunque esto funcionaba, sin el adecuado soporte de un depurador en el entorno Visual Basic el desarrollo de código multiprocesamiento era casi una pesadilla. En este capítulo presentaremos los distintos objetos de la plataforma .NET que permiten que cualquiera de los lenguajes .NET pueda utilizarse para desarrollar aplicaciones multiproceso. ¿Qué es un subproceso? El principio de un subproceso se basa en permitir que partes de nuestro programa se ejecuten independientemente de otras. Tal como probablemente sepa, en Windows (NT, 2000 o XP) nuestro programa se ejecuta en un proceso separado. Ésta es una división artificial que brinda aislamiento entre los programas, de manera que un problema en un programa no puede afeptar la operación de otro (volveremos sobre esto más adelante). Un subproceso es en efecto un puntero de ejecución, que permite que Windows controle en cada momento cuál es la línea de nuestro programa que se está ejecutando. Este puntero comienza con el principio del programa y se mueve línea a línea, realizando bifurcaciones e iteraciones cuando se encuentra con decisiones y bucles, y cuando el programa ya no es necesario, el puntero sale del código del programa y el programa se detiene efectivamente. En los subprocesos se tienen múltiples punteros de ejecución. Esto significa que dos o más partes de nuestro código se pueden ejecutar simultáneamente. Un clásico ejemplo de la funcionalidad de multiproceso es el corrector ortográfico de Microsoft Word. Cuando se inicia el programa, el puntero de ejecución comienza con la parte superior del programa y finalmente se llega a una situación en la que podemos empezar a escribir texto. Sin embargo, en algún punto Word comienza otro subproceso y crea otro puntero de ejecución. A medida que tecleamos el texto, este nuevo subproceso examina el texto e indica la existencia de errores ortográficos, subrayándolos con una línea ondulada de color rojo: Capítulo 19 Cada aplicación tiene su subproceso principal. Este subproceso sirve como subproceso principal durante toda la aplicación. Imaginemos que tenemos una aplicación que comienza, carga un archivo desde el disco, realiza algún proceso con los datos del archivo, escribe un nuevo archivo y luego termina. Funcionalmente, esto podría tener este esquema: En esta aplicación de ejemplo, sólo necesitamos utilizar un único subproceso. Cuando se indica que el programa se ejecute, Windows crea un nuevo proceso y también crea el subproceso principal. Para comprender más acerca de lo que es realmente un subproceso, es necesario entender un poco sobre cómo Windows y el procesador del equipo tratan con los diferentes procesos. Procesos comparados con subprocesos Windows tiene la capacidad de mantener muchos programas en memoria simultáneamente y permite que el usuario pueda pasar de uno a otro. Estos programas se denominan aplicaciones y servicios. La diferencia entre aplicaciones y servicios es la interfaz de usuario: los servicios normalmente no tienen una interfaz de usuario que permita al usuario interactuar con ellos, mientras que las aplicaciones sí que tienen sus interfaces de usuario (en tal caso, Microsoft Word es un ejemplo de una aplicación, mientras que Internet Information Server es un ejemplo de un servicio). La capacidad para ejecutar muchos programas simultáneamente se denomina multitarea. Cada uno de estos programas que nuestro equipo mantiene memoria se ejecuta en un proceso. El proceso se inicia cuando se inicia el programa y existe durante el tiempo en que el programa se ejecuta. Como Windows es un sistema operativo que soporta multiproceso, un programa puede crear subprocesos separados dentro de su propio proceso. Sin embargo, multitarea y multiproceso no es necesariamente lo mismo. 704 Subprocesos Multitarea significa que el sistema operativo puede mantener simultáneamente en memoria múltiples programas y darles una oportunidad para que se ejecuten (más adelante seguiremos tratando esto), pero multiproceso específicamente significa la capacidad para crear más de un subproceso dentro de un proceso. Para soportar un entorno multitarea, tanto el sistema operativo como el procesador tienen que trabajar en conjunto para dividir la potencia de cálculo disponible entre todos los procesos en ejecución. Ahora realizaremos una simplificación de nuestra descripción sobre cómo Windows divide el tiempo de procesamiento ya que un detalle minucioso de este tema está fuera del alcance de este libro. Sin embargo, realizaremos un análisis general de cómo funciona la subdivisión del tiempo. Supongamos que tenemos dos procesos en ejecución en Windows. En un determinado período de tiempo, Windows dará el 50% de su potencia de proceso al subproceso principal del primer proceso y el 50% restante al subproceso principal del segundo proceso. La división de la potencia de proceso nos conduce al concepto de prioridad de proceso. La asignación de prioridad en un proceso le indica a Windows que debe dedicarle más tiempo al proceso que tenga mayor prioridad. Esto resulta útil en situaciones donde tenemos un proceso que requiere mucha potencia de cálculo, pero no importa cuánto tarda el proceso en realizar su trabajo. Un ejemplo de esto es el Proyecto de Investigación del Cáncer de Intel/United Devices. Este proyecto se basa en tener miles de ordenadores en todo el mundo ejecutando un algoritmo que intenta buscar la coincidencia entre las moléculas de drogas con proteínas asociadas con la propagación del cáncer. Este programa se ejecuta continuamente, pero las rutinas de cálculo requieren una gran capacidad de uso del procesador para los cálculos matemáticos. Sin embargo, este proceso se ejecuta con muy baja prioridad, por lo tanto, si necesitamos utilizar Word, Outlook u otra aplicación, Windows brinda más tiempo de procesador a estas aplicaciones y menos tiempo a la aplicación de investigación. Esto significa que el ordenador puede funcionar adecuadamente cuando el usuario lo necesita, dejando que la aplicación de investigación aproveche el tiempo restante. En el sitio web http:/www.ud.com/ se pueden conocer más detalles sobre el Proyecto de Investigación del Cáncer de Intel/United Devices. Supongamos que tenemos en nuestro fragmento de tiempo una granularidad de tres segundos, en otros términos, dado un fragmento de tiempo dividido entre dos procesos, el proceso A se ejecuta durante un segundo y medio y después el proceso B se ejecuta durante un segundo y medio. Al final del período, el proceso A tiene otra oportunidad de ejecutarse durante un segundo y medio y después el proceso B tiene su oportunidad para ejecutarse durante un segundo y medio. Si se inicia un tercer proceso, los procesos A, B y C todos tienen la oportunidad de ejecutarse durante un segundo. Si el proceso B y el proceso C finalizan, el proceso A tiene todo el tiempo del procesador para sí mismo, hasta que empiece algún otro proceso. Lo que es importante entender acerca de la división del tiempo es que los procesos no tienen que participar activamente en este proceso. Si tenemos tres procesos ejecutándose y una granularidad de tres segundos, al final de la primera sección el proceso A no tiene que decir a Windows "Muy bien, se me acabó el tiempo, esperaré". En cambio, lo que sucede es que Windows detiene la reserva de tiempo para el proceso, y, efectivamente, lo detiene hasta que tiene otra oportunidad para ejecutarse. Esto se conoce como multitarea preferente. 705 Capítulo 19 La división del tiempo también se aplica a los subprocesos. Tal como se sabe, cuando se inicia un proceso se le da un subproceso principal. A medida que se ejecuta el subproceso tiene la posibilidad de crear otros subprocesos. Imaginemos ahora que el proceso A y el proceso B ambos tienen un único subproceso, pero el subproceso principal del proceso C ha iniciado otro subproceso y tiene en total dos subprocesos: En este ejemplo, el proceso C está obteniendo un tercio de la potencia de proceso en cada división de tiempo. Sin embargo, como el proceso C está compuesto de dos subprocesos, el primer subproceso está obteniendo la primera mitad de un tercio (17%) y el segundo subproceso está obteniendo la segunda mitad de un tercio (nuevamente, 17%). Por lo tanto, al iniciarse el tercer segundo, Windows detiene la ejecución del proceso B e inicia la ejecución del primer subproceso del proceso C. Al transcurrir medio segundo (es decir, a los dos segundos y medio de haber iniciado el fragmento de tiempo), Windows suspende el primer subproceso del proceso C y pasa al segundo subproceso del proceso C. Al finalizar el tercer segundo, se suspende el segundo subproceso del proceso C y se reinicia el proceso A (cabe señalar que esto es una gran simplificación). Esto queda ilustrado en la siguiente figura: 706 Subprocesos Entonces, ¿por qué todo esto es importante? Bien, la división del tiempo nos conduce al punto donde todo el poder de procesamiento se comparte igualmente. Sin esta división del tiempo podemos finalizar fácilmente en una situación donde estamos a merced del mal comportamiento de los procesos. Si Windows no fuese capaz de decir "Muy bien, es tiempo de suspender este proceso y de pasar a un proceso diferente", entonces tendría que decir "Esperaré hasta que este proceso parezca estar ocioso, ya que al estar ocioso se supone que ha terminado su tarea, y entonces comenzaré la ejecución de este otro proceso". Si Windows confiase en que los procesos se comportan correctamente, sería muy sencillo que un proceso secuestrara el sistema y diese una prioridad superior a su propio proceso. Esto puede tener un efecto catastrófico si el proceso ha entrado en un bucle infinito, como el siguiente: n = 2 Do While n = 2 Loop Si el sistema operativo estuviese esperando que este proceso quede ocioso antes de iniciar otro proceso, ningún otro proceso tendría la oportunidad de realizar su trabajo. Esto bloquearía todo el equipo. Esta situación se describe como multitarea cooperativa. Éste fue el paradigma de la multitarea utilizado por las versiones de Windows de 16 bits, por ejemplo, Windows 3.1. En él se dependía del programa para que diese al sistema operativo la oportunidad de ejecutar otro programa. La otra ventaja de este esquema es que el sistema operativo se ejecuta con menos conflictos. Como Windows está tomando un interés activo sobre cómo se ejecutan los procesos, ningún proceso obtiene una distribución inadecuada del poder de procesamiento. Aunque esto pueda parecer un concepto un poco nebuloso, esta falta de conflictos es una de las razones del uso de los subprocesos. Cuándo utilizar subprocesos Si consideramos los programas de computación como software de aplicación o software de servicio, nos encontraremos que existen diferentes motivadores para cada uno de ellos. 707 Capítulo 19 El software de aplicación utiliza principalmente subprocesos para brindar al usuario una mejor experiencia. Ejemplos comunes son los siguientes: q Microsoft Word: Comprobador ortográfico en segundo plano q Microsoft Word: Impresión en segundo plano q Microsoft Outlook: Envío y recepción de correo electrónico en segundo plano q Microsoft Excel: Recálculo en segundo plano Se puede ver que en todos estos casos, los subprocesos se utilizan para hacer "algo en segundo plano". Esto brinda al usuario una mejor experiencia. Por ejemplo, puedo editar un documento Word mientras Word está enviando una salida a la impresora. O, puedo leer mensajes de correo electrónico mientras Outlook está enviando mi nuevo correo. Como desarrollador de aplicaciones, deberíamos utilizar subprocesos para optimizar la experiencia del usuario. A continuación se muestra un diagrama que ilustra los subprocesos explicados cuando se utiliza el comprobador ortográfico de Word: En algún momento durante el inicio de la aplicación, el código que se ejecuta en el subproceso principal habrá iniciado este otro subproceso para utilizar la comprobación ortográfica. Como parte del proceso "permitir que el usuario edite el documento", le damos al subproceso de comprobación ortográfica algunas palabras para su verificación. Esta separación de subprocesos significa que el usuario puede continuar tecleando, incluso aunque el corrector ortográfico esté aún haciendo su tarea. El software de servicio utiliza los subprocesos para ofrecer servicios mejorados y escalables. Por ejemplo, supongamos que tenemos un servidor web que recibe seis conexiones entrantes simultáneamente. Ese servidor necesita servir a cada una de estas peticiones en paralelo, pero el sexto subproceso tendría que esperar la finalización de alguno de los primeros subprocesos para poder comenzar. Así es como podría gestionar IIS estas peticiones entrantes: 708 Subprocesos En este diagrama tenemos seis subprocesos que existen sólo para colaborar con las peticiones de los clientes. Cada petición es gestionada por un subproceso. Este diagrama nos muestra los otros subprocesos de la aplicación que gestionan estos subprocesos. Por ejemplo, el subproceso principal de la aplicación esperará la indicación de que el servicio debe ser detenido por alguna razón. Otro subproceso estará atendiendo las conexiones entrantes y creará un nuevo subproceso, o buscará un subproceso existente que pueda servir a esas peticiones (la mayoría del software del servidor utiliza agrupaciones de subprocesos, un tema que trataremos al final de este capítulo). El desarrollo ASP y ASP.NET es un ejemplo claro de una situación donde no nos importan los subprocesos, aunque estemos ejecutando en un entorno multiproceso. Cada vez que se llama a una página .asp o .aspx estaremos ejecutando de un modo bastante aislado: no nos importa lo que están haciendo los otros subprocesos. Incluso con ASP.NET el procesamiento se realiza de modo bastante lineal, en otras palabras, desde el inicio de la página hasta la parte inferior de la página. Un ejemplo de subprocesos En .NET la creación de un subproceso es extremadamente sencilla. Todo lo que tenemos que hacer es crear un ejemplar del objeto System.Threading.Thread y llamar al método Start. Sin embargo, para hacer esto necesitamos una aplicación de ejemplo. Lo que generaremos es una aplicación sencilla con un botón y un cuadro de texto. Cuando se haga clic en el botón iniciaremos el subproceso y desde dentro del botón asignaremos el texto al cuadro de texto. Crear un nuevo proyecto Aplicación para Windows y denominarlo ThreadExample. Después de la creación del proyecto, el diseñador de formularios abrirá el formulario predeterminado Form1. Incluir 709 Capítulo 19 en el formulario Form1 un botón denominado btnStartThread y un cuadro de texto denominado txtResult: Hacer doble clic en el fondo del formulario para crear un nuevo gestor del evento Load. Añadir el siguiente código: Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load Me.Text &= " - Thread #" & _ System.Threading.Thread.CurrentThread.GetHashCode End Sub La propiedad compartida CurrentThread se puede utilizar para acceder a un objeto Thread que representa el subproceso actualmente en ejecución. Como este código se estará ejecutando dentro del subproceso principal, este objeto Thread representará al subproceso principal. GetHashCode retorna un identificador único del subproceso. Cuando ejecutamos el código, en el título se visualizará el identificador del subproceso principal. Después de hacer esto, podemos ver cómo se consigue la ejecución del subproceso. Creación de ThreadWorker Mi método preferido para gestionar subprocesos es crear una clase separada para gestionar el inicio y la detención de un subproceso. Esta clase contiene un miembro privado denominado _thread que contiene un ejemplar de un objeto System.Threading.Thread. Los ejemplares de esta clase colaboradora se pueden generar mediante la aplicación principal y se pueden controlar exclusivamente mediante sus métodos. Esto encapsula la funcionalidad del subproceso, lo que significa que el llamante no tiene que preocuparse en realidad por el control del ciclo de vida del subproceso. Crear una nueva clase denominada ThreadWorker y añadir este código: Imports System.Threading Public Class ThreadWorker Public TextBox As TextBox Private _thread As Thread End Class 710 Subprocesos ThreadWorker tendrá un método público denominado SpinUp que iniciará el subproceso. Pero, para que el subproceso se inicie, necesitamos suministrar un punto de entrada mediante un delegado que hace referencia a un método en nuestra clase ThreadWorker. El subproceso llamará a este delegado tan pronto como esté listo para iniciar el trabajo. Esto es conceptualmente similar al método de inicio que tenían los proyectos VB6. De hecho, el método de inicio es el punto de entrada del subproceso principal. Como es improbable que otros subprocesos quieran utilizar el mismo punto de entrada, tenemos que dar uno nuevo; éste es nuestro trabajo aquí. Nuestro delegado se llamará Start (podemos elegir el nombre que más nos guste, preferí Start) y se define de la siguiente manera: Private Sub Start() TextBox.Text = "Hello, world from thread #" & _ Thread.CurrentThread.GetHashCode() & "!" End Sub Nuevamente, aquí estamos utilizando CurrentThread para obtener un objeto Thread que está ejecutando el subproceso actual. Como este código debería ejecutarse desde dentro del nuevo subproceso, el identificador no debería coincidir. Se puede ver que no tenemos que hacer nada especial desde dentro de la subrutina Start para brindar soporte al subproceso. Recordemos, ya hemos estado escribiendo código que se ejecuta en un subproceso: sólo que hasta ahora hemos tenido un único subproceso. Éste es el código para el método SpinUp dentro de ThreadWorker que iniciará el subproceso: Public Sub SpinUp() ' crea un nuevo objeto de inicio de subproceso ' que hace referencia a nuestro worker... Dim threadStart As ThreadStart threadStart = New ThreadStart(AddressOf Me.Start) ' ahora, creamos el objeto subproceso y lo ' iniciamos... _thread = New Thread(threadStart) _thread.Start() End Sub Y esto es todo. Para iniciar el subproceso todo lo que tenemos que hacer es crear un objeto System.Threading.ThreadStart, darle un delegado que haga referencia a nuestro punto de entrada, y pasarle un nuevo objeto System.Threading.Thread. Cuando llamemos a Thread.Start se creará un nuevo subproceso y como punto de entrada se llamará al método delegado ThreadWorker.Start. 711 Capítulo 19 Llamada a SpinUp Para llamar a SpinUp necesitamos incluir algo del código subyacente en el botón. Añadir este código: Private Sub btnStartThread_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles btnStartThread.Click ' crea un nuevo worker... Dim worker As New ThreadWorker() worker.TextBox = txtResult ' inicia el subproceso... worker.SpinUp() End Sub Como txtResult es un miembro privado, no podemos acceder a él directamente desde ThreadWorker. En cambio, utilizamos la propiedad TextBox para pasar un control cuadro de texto a nuestro ejemplar ThreadWorker. El código que se ejecuta dentro del subproceso puede entonces asignar la propiedad Text de nuestro control para actualizar el texto que se visualiza en pantalla. Para probar la solución, ejecutar el proyecto y hacer clic en el botón Start Thread. Veremos algo parecido a esto: Se puede ver que el identificador mostrado en la barra de título y en el cuadro de texto son diferentes. Esto prueba que el código que asigna el título y el código que asigna el texto en el cuadro de texto se están ejecutando en subprocesos diferentes. Ahora veremos por qué el desarrollo de aplicaciones multiproceso requiere más que un enfoque improvisado o informal. Sincronización Windows suministra aislamiento entre los distintos procesos, esto significa que un proceso no puede afectar directamente a otro proceso, o, mejor dicho, un proceso sólo puede ser afectado de una manera controlada. Por ejemplo, si se ejecutó un proceso que terminó anormalmente, en teoría todos los otros procesos deberían continuar ejecutándose. Si se ejecutase un proceso que tratase abarcar todo el procesador para sí mismo, los otros procesos continuarían ejecutándose. 712