Subprocesos

Anuncio
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
Descargar