This entry is part 1 of 3 in the series: Programación Multihilo en Delphi

Antes de empezar

En este articulo no hay ni una sola linea de código y no esta orientado a enseñar los entresijos especificos de la programación multihilo en este o aquel lenguaje sino a dar una pequeña introducción, centrandose fundamentalmente en el como y el sobre todo en el por qué y en el cuando.

¿Que es la programación multihilo?

En estos tiempos todos los sistemas operativos (excepto quizá los educacionales) son sistemas operativos multitarea, esto quiere decir que pueden ejecutar varias tareas o procesos simulataneamente. Esto no siempre fue así, el ms-dos era un sistema monotarea lo que quiere decir que tan solo una tarea podía ejecutarse en el sistema simultaneamente.

Antes de lanzarnos a explicar la programación multihilo es conveniente hacer un repaso a ciertas caraterísticas de los sitemas operativos.

MultiTarea y MultiProcesador. El planificador.


Cuando hablamos de sistemas operativos multitarea decimos que pueden ejecutar varias tareas simultaneamente de forma concurrente. Esto no es del todo cierto, tan solo los sistemas multiprocesador (con más de un procesador) pueden realizar multitarea real, en un sistema con un solo procesador hablamos de sensación de multitarea.

En ambos casos interviene un mecanismo del sistema operativo denominado planificador. El planificador del sistema operativo es el que se encarga de administrar las distintas tareas que están en ejecución y decidir cuando ejecuta la tarea y – si es aplicable – en que procesador lo hace. Cuando una tarea está en ejecución tiene disponible la totalidad del procesador, no lo comparte con ninguna otra, lo mismo ocurre con el espacio de memoria, para la tarea que esta ejecutando dispone de toda la memoria disponible del sistema.

Para que haya realmente multitarea o por lo menos sensación de ella, las tareas deben compartir el procesador, es decir, eventualmente la tarea que esta en ejecución en un determinado momento deberá retirarse y dejar paso a otra tarea que ocupara su lugar, esto – de lo que también se ocupa el planificador – se denomina cambio de contexto y, entre otras cosas implica una cierta sobrecarga de recursos (hay que guardar ciertos datos relativos al proceso que sale y cargar otros tantos relativos al proceso que entra).

Espacio de memoria del proceso.

El concepto de espacio de memoria de un proceso está relacionado con el concepto de multitarea. Cada tarea presente en un instante dado tiene su propio espacio de memoria, es decir, los datos del proceso (variables, la pila, el código) están en un espacio lógico separado del resto de los demás de forma que, para los demás procesos todos esos datos no existen (de hecho el resto de procesos no es «consciente» de que hay más procesos, si quiere saberlo debe preguntarselo siempre al sistema operativo – mediante una llamada al sistema).

Hilos dentro de procesos.

Cuando hablamos de programación multihilo estamos haciendo referencia a que, dentro de nuestra propia aplicación haya diversas tareas ejecutando, es decir, nuestro proceso reparte de alguna forma el trabajo entre distintas tareas hijas.

En este sentido hay que resaltar que para el sistema operativo seguimos siendo un solo proceso (aunque el sistema operativo «sabe» que tenemos varias tareas hijas), de forma que el tiempo asignado por el sistema operativo (nuestra rodaja de tiempo) no varía y por otro lado tan solo una de las tareas hijas de nuestro proceso se estará ejecutando en un momento dado.

¿Cual es entonces la diferencia?. La diferencia radica en que, cuando un proceso deja de ejecutar y entra a ejecutar otro proceso se produce un cambio de contexto con el consiguiente desalojo de información del proceso (ver Apendice), sin embargo cuando se produce el mismo caso entre tareas hijas dicho cambio de contexto no tiene lugar. Esto es debido a que dichas tareas comparten el mismo espacio de memoria, el del proceso padre.

¿Por que y cuando programar el multihilo?

Decidir cuando se debe orientar una aplicación hacia el multihilo no es una decisión sencilla. En muchos casos la aplicacion es lo suficientemente simple como para que no sea necesario siquiera plantearselo, en otros casos en que la aplicación es lo suficientemente compleja quizá la sobrecarga o la complejidad introducida por el paso a multihilo anule las posibles ventajas que pudiera generar.

En general la decisión de usar varios hilos debe tomarse cuando existan tareas bien definidas con un coste computacional alto (es decir tareas en las que el coste computacional sea mucho mayor que el de un cambio de contexto), en las que buena parte de su ejecución no dependa del resultado del resto de tareas (puesto que no tedría sentido programar tres tareas que van a estar continuamente esperandose las unas a las otras) y que además se prevea que puedan ser «retenidas» en ciertos momentos de su ejecución (por lecturas de disco, accesos a base de datos …).

Las razones para estos tres aspectos radican en lo explicado anteriormente sobre el modelo de ejecución del sistema operativo.

  1. Si el coste computacional de una tarea no es alto el coste de introducir cambios de contexto hace que desaparezca cualquier mejora que pudieramos conseguir gracias al multihilo.
  2. Si una tarea depende de otra en gran medida (por ejemplo necesita el resultado que proporciona otra tarea para empezar) entonces no se gana nada ya que si la tarea B necesita que la tarea A termine para poder empezar la concurrencia no se producirá, acabará la tarea A y a continuación empezará la tarea B (que es lo mismo – de hecho es peor – que si la tarea A acabara la primera fase y ella misma comenzara la segunda).

Tareas retenidas.

Cuando hablamos de tarea retenida (lo que normalmente se conoce como bloqueada) hablamos de una tarea que esta esperando a que un recurso externo complete una operación.

Hay determinadas operaciones que, por las limitaciones de los dispositivos a los que acceden, son mucho más lentas que una operación normal de la CPU. Esto es especialmente cierto en el caso de los accesos a disco y los accesos a maquinas remotas. Cuando un proceso (o tarea) realiza uno de estos accesos pasa al estado bloqueado y entra en ejecución otra tarea. De esta forma, si nuestra aplicación es previsible que se quede bloqueada durante un determinado tiempo x (por que por ejemplo esta accediendo a una base de datos) que puede ser utilizado para hacer otras cosas es recomendable plantearse dividir la aplicación en dos tareas para aprovechar ese «tiempo muerto» que se produce al acceder a la base de datos.

En cambio, si nuestra aplicación no se va a ver afectada por este tipo de accesos (o no hay realmente nada que hacer excepto esperar a que acaben) deberemos considerar como mejoraría según otros supuestos (tecnología de hyperthreading, varios procesadores, varios nucles) ya que en principio, en maquinas con un solo procesador, no va a experimentar una mejora sustancial de rendimiento global.

Un ejemplo de mejora de rendimiento.

Un ejemplo de una situación en la que sería recomendable el uso de varios hilos de ejecución podría ser una situación en la que tuvieramos que acceder a varias base de datos situadas en distintos servidores, operar con los resultados proporcionados y volver a acceder a las bases de datos. Con una sola tarea esto supone

  1. Acceder a la primera base de datos.
  2. Realizar la consulta y esperar a los resultados
  3. Esperar datos
  4. Esperar datos
  5. Operar
  6. Acceder a la segunda base de datos
  7. Realizar la consulta y esperar a los resultados
  8. Esperar datos
  9. Esperar datos
  10. Operar

En cambio si se implementa un hilo por consula podrán realizarse los accesos y consultas a las bases de datos de forma concurrente, si dichas base de datos están situadas en distintos servidores esos servidores estarán obteniendo nuestra información de forma concurrente es decir, tendríamos lo siguiente:

  • (Tarea1) Acceder a la primera base de datos
  • (Tarea1) Realizar la consulta (Tarea2) Acceder a la segunda base de datos
  • (Tarea2) Realizar la consulta (Tarea1) Esperar datos
  • (Tarea1) Esperar (Tarea2) Esperar datos
  • (Tarea1) Operar
  • (Tarea2) Operar

El acceso a la primera base de datos no mejor mucho pero sin embargo el tiempo de acceso a la segunda base de datos sufre una mejora enorme debido a que hemos accedido de forma paralela.

Un ejemplo de separación conceptual.

Aunque hasta ahora hemos venido centrandonos en el concepto de mejora de rendimiento mediante el uso de hilos diferenciados, en ocasiones es interesante separar la aplicación en varios hilos aunque la ganancia de rendimiento no sea muy grande si con ello se consigue una separación conceptual adecuada o si queremos garantizar que determinada parte de la aplicación ejecuta con mayor prioridad.

Por ejemplo si la aplicación tiene una parte de log, una parte de gestión de música y una parte de soporte vital, parece conveniente separarla en 3 hilos y asignar la más alta prioridad a la parte del soporte vital.

Otro ejemplo podría ser el diseño de un videojuego en el que por ejemplo pudieramos distinguir partes como el tratamiento de la entrada del usuario, el renderizado de imagenes, el sistema de inteligencia artificial… que conceptualmente parecen tareas que deberían realizarse de forma simultanea y no una detras de la otra.

Además de todo eso nunca hay que despreciar la posibilidad de que la aplicación se ejecute en máquinas con más de un procesador (o con alguna tecnología como hyperthreading). Una aplicación que podía no ganar nada en una maquina con un solo procesador (por que las tareas nunca hacian entrada/salida por ejemplo) podría ganar mucho si hay dos procesadores disponibles y cada hilo puede ejecutar en un procesador difereciado, por ejemplo el videojuego podría estar calculando la trayectoria de caida de un solido mientras la inteligencia artificial decide si activa o no los propulsores de estribor.

Apendice

Cambios de contexto.

En un sistema operativo multitarea el procesador está «repartido» entre las distintas tareas en ejecución. Cada tarea tiene una serie de datos asociados y que son exclusivos de dicha tarea. Estos datos están agrupados en una estructura llamada BCP (bloque de control de proceso). En el BCP pueden encontrarse, entre otras cosas:

  • El identificador del proceso (pid).
  • El estado del proceso (en ejecución, listo, bloqueado)
  • Información sobre la entrada/salida
  • Información de gestión de memoria
  • Estado de los registros de la máquina (puntero de programa y puntero de pila)

Además del BCP, parte de la información del proceso consiste en el conjunto residente. El conjunto residente de un proceso esta constituido por los datos de un proceso que se encuentran actualmente en memoria principal (puede que haya datos que se encuentren en el disco, en una sección conocida como swap, que es mucho más lenta que la memoria principal).

Cuando se produce un cambio de contexto, se guarda el BCP del proceso saliente , se carga el BCP del proceso entrante y el proceso entrante comienza a ejecutar. Si el proceso anterior había desalojado datos del proceso que acaba de entrar (por que necesitaba espacio para los datos propios), los datos del proceso que entra estarán ubicados en la swap, en el disco duro, por lo que será necesario volver a traerlos a memoria principal, lo cual es una operación lenta. Es posible que al traer esos datos estemos desalojando los datos de otro proceso del sistema que entrará a ejecutar en un futuro.

Introducción a la planificación del SO.

Existen varios motivos por los que un proceso que está en ejecución puede dejar de estarlo.

El proceso consume su rodaja de tiempo

En la mayoría de los sistemas operativos, el tiempo de la CPU está organizado en rodajas de tiempo, cuando un proceso entra a ejecutar en la CPU dispone de un periodo de tiempo máximo, transcurrido el cual es expulsada para permitir que entre a ejecutar otro proceso.

Para que esto pueda producirse existe un mecanismo de hardware que actua como un temporizador levantando lo que se conoce como TRAP al sistema operativo que es una excepción que hace que la tarea que esta ejecutando se bloquee y pase a ejecutar un segmento del sistema operativo que se encargará de desalojar la tarea actual, ejecutar el planificador, que decidirá que tarea debe ejecutar siguiente, y cargar dicha tarea.

Un proceso realiza una operación bloqueante

Si un proceso que está en ejecución realiza una operación de e/s, por ejemplo un acceso a disco, o el acceso a una zona de memoria cuya página no está en memoria principal, automáticamente pasará al estado bloqueado (hasta que termine la operación de e/s, momento en el cual pasará a estado listo) y otro proceso ocupará su lugar.

Se produce una exepción en la ejecución del proceso

Por último, otra de las formas en las que un proceso puede dejar la ejecución es cuando se produce una excepción (por ejemplo un acceso a memoria privilegiada o una división por cero).

Series Navigation

Programación multihilo en Delphi. TThread y sincronización básica >>