TECNOLOGÍA - MICROTUTORIALES: PROCESOS Y THREADS
ABSTRACT
En este microtutorial se pretende explicar el concepto de proceso y, sobre todo, el de thread (hilo de ejecución). Para ello se repasan algunos conceptos previos acerca de programas, su ejecución mediante microprocesadores y conceptos de procesamiento paralelo.
TUTORIAL
Procesos y threads

¿ Qué es un programa ?
No es fácil, a pesar de lo habitual del término, dar una definición exacta, universalmente admitida y rigurosa de lo que es un programa.

Podemos pensar en un programa como una unidad de software coherente, tanto en su creación, como almacenamiento y ejecución que, ejecutada por un computador digital realiza una tarea de valor para algún tipo de usuario.

En el contexto en el que hablamos, un programa es equivalente a una aplicación. Así, podríamos denominar programas a una hoja de cálculo, un editor de figuras, un software de retoque fotográfico, etc, En todos estos ejemplos, se ha creado el software como un todo por parte de un equipo de trabajo, se almacenan y distribuyen todos lo ficheros de manejar conjunta (en un CD, un DVD, un disco duro…) y se ejecutan como una unidad coherente y coordinada.

Es preciso hacer una advertencia: en entornos Mainframe y lenguaje COBOL, el concepto de programa tiene otra significación, pero que no trataremos aquí.

Variables y funciones
Aunque existen muchos modelos de programación y de ejecución, para comprender lo que viene acerca de procesos, consideremos un modelo de ejecución como el que se muestra en la figura (inspirado en un programa estructurado escrito en C):


Cuando un microprocesador debe ejecutar un programa, primero prepara una zona de memoria (en el modo que ya veremos) para acoger al programa y luego lo ejecuta, es decir, procesa las instrucciones que componen el programa.

El microprocesador, mientras ejecuta un programa, necesita almacenar algunos pequeños datos para su uso exclusivo. Así, por ejemplo, necesita almacenar un elemento denominado contador de programa que le indica cuál es la siguiente instrucción que tiene que ejecutar. Este conjunto de pequeñas informaciones que utiliza el microprocesador es lo que se denominan registros del microprocesador.

Durante la ejecución de un programa es necesario almacenar informaciones propias del programa, que se utilizarán en operaciones posteriores o que constituyen ya resultados intermedios o finales del procesamiento. Estas informaciones son lo que se denomina variables. En un programa, la existencia de una variable se declara durante la programación (es decir, al iniciar el programa ya se sabe que va a existir) y durante la ejecución se actualiza el valor.

Los programas modernos son muy complejos, por lo que no constituyen un todo monolítico sino que se estructuran en unidades de lógica que reciben unos datos de entrada, realizan un procesamiento, a veces almacenando valores intermedios en variables, y devuelven datos de salida a quien las invocó. Aunque hay otros modelos y denominaciones, en la figura hemos denominado a esas unidades con el nombre de funciones.

La ejecución de un programa se inicia con la ejecución de una función principal (la que en el lenguaje C se denomina main()). A medida que progresa, la función principal puede necesitar llamar a otras funciones. En la figura, la función principal invoca a tres funciones que hemos denominado funcion1(), funcion2() y funcion3(). A su vez, vemos que funcion2() necesita invocar a otra función que hemos denominado funcion21(). Esta cadena de invocaciones se puede anidar de manera indefinida. En cada invocación, la función invocada recibe unos datos, parámetros o argumentos de entrada, maneja internamente unas variables y devuelve unos resultados, parámetros o argumentos de salida.

Vemos, pues, que se manejan variables en distintos niveles: en el conjunto del programa y en cada función en su nivel de anidamiento. Las variables que son comunes a todo el programa se denominan variables globales. Las que son internas a una función se denominan variables locales.

Hemos dicho que las variables están declaradas y se conocen en el inicio del programa. A veces, sin embargo, es necesario reservar y utilizar memoria para el almacenamiento de datos a lo largo del programa de una manera no declarada ni prevista al inicio del programa. En este caso, estamos ante el uso de lo que se denomina memoria dinámica (recordar la típica instrucción ‘malloc()’ del lenguaje C o ‘new’ de Java para reservar memoria).

Programas y segmentos de memoria
Vemos, pues, que cuando se ejecuta un programa, es necesario llevar el código del programa a memoria, es necesario reservar espacio de memoria adicional para variables globales, es necesario prever la creación de variables locales y el paso de argumentos cuando se invocan funciones y es necesario habilitar un mecanismo para proporcionar memoria de manera dinámica. Además, el microprocesador mantiene sus pequeñas áreas de información propias en los registros.

Aunque la forma de gestionar esto depende de cada microprocesador y sistema operativo, la política general que se sigue, es la que se ilustra en la siguiente figura:


Cuando es necesario ejecutar un programa, se reserva primero un espacio de memoria para esa ejecución del programa. Ese espacio de memoria se divide en varias partes o segmentos:
  • Segmento de programa: En él se almacena el código de programa.
  • Segmento de datos: En él se reserva espacio para todas las variables conocidas del programa, típicamente, las variables globales.
Estos dos segmentos mantienen un tamaño ocupado constante a lo largo de la ejecución del programa. No ocurre así con los dos siguientes:
  • Segmento de pila (stack): este segmento se utiliza para mantener un estructura dinámica de datos, que implementa una pila LIFO (Last In, First Out) y en donde, típicamente, se almacenan los datos de entrada de las funciones, las variables locales y los datos de salida. La pila tiene un cierto dinamismo puesto que, según se aniden más o menos llamadas a funciones, se va llenando con valores de más o menos argumentos de entrada/salida y variables locales.
  • Montículo (heap): Es la zona de donde se toma la memoria dinámica que pueda solicitar el programa. Puede ser usada o no y en mayor o menor cuantía según las características del programa y de la ejecución específica.
Ejecución paralela y pseudoparalela
En los ordenadores y sistemas operativos antiguos, era posible que el sistema operativo ejecutase un solo programa cada vez. Así ocurría, por ejemplo, con el MS-DOS sobre procesadores Intel como el 8086.

Sin embargo, esta no es la situación ni óptima ni habitual hoy día. En los sistemas modernos, se ejecutan varios programas a la vez (un navegador web, un procesador de textos, una calculadora, etc) e, incluso, varias instancias del mismo programa a la vez (por ejemplo abrir dos veces, es decir, creación de dos instancias del navegador web).

En este caso nos hallamos ante un mutiprocesamiento o ejecución paralela.

No obstante, es preciso distinguir dos situaciones:
  • Ejecución paralela propiamente dicha: Se puede dar en ordenadores con varios procesadores. Cada microprocesador puede estar ejecutando un programa distinto. En ese caso, realmente, se están haciendo varias labores a la vez.
  • Ejecución pseudoparalela o de tiempo compartido: Este caso se da, típicamente, en ordenadores con un solo procesador pero con un sistema operativo multitarea. En este caso, lo que ocurre es que el tiempo del microprocesador se reparte alternativamente entre los diferentes programas en ejecución pero, en un momento dado, el microprocesador y, por tanto, el ordenador, sólo está atendiendo a un programa concreto. Lo que ocurre es que el cambio entre programas se produce con tal velocidad que para el usuario humano se produce la ilusión de que se están realizando varias tareas en paralelo. Es la situación más habitual, por ejemplo, en PCs monoprocesador ejecutando Windows.
Procesos
Cuando consideramos el multiproceso es cuando el concepto de proceso comienza a adquirir sentido. Hasta ahora hemos hablado tranquilamente de programa sin más distinción pero, para ser rigurosos, cuando hablábamos de los segmentos de un programa en ejecución, deberíamos haber hablado de los segmentos de un proceso en ejecución. Básicamente, podemos decir que un proceso es un programa en ejecución. Consideremos el caso del navegador web. Se trata de un programa. Si lo ejecutamos tendremos un proceso que está ejecutando ese programa. Si ahora lanzamos de nuevo el navegador, el programa es el mismo, pero existirá una segunda instancia en ejecución, es decir, un segundo proceso. Tendremos un solo programa pero dos procesos.

Siguiendo el modelo de segmentos que explicamos más arriba, cada vez que se lanza la ejecución de un programa, se crea un proceso, con su segmento de programa (copia del código del programa), su segmento de datos, su pila y su heap. Si lanzamos una nueva instancia, se creará un nuevo segmento de programa, un nuevo segmento de datos, una nueva pila y un nuevo heap.

Como cada proceso tiene zonas de memoria diferentes, puede evolucionar en paralelo almacenando los valores concretos de sus variables, y argumentos de entrada y salida.

Con este modelo de procesos se funcionó muchos años en los sistemas multitarea como, por ejemplo, UNIX.

Threads
Los procesos, presentan alguna carencia en cuanto a eficiencia. Si lanzamos varias veces el mismo programa, es decir, tenemos varias instancias o procesos, será necesario por cada una de ellas, leer el código de programa de almacenamiento secundario (disco), llevarlo a memoria, preparar los segmentos, etc.

Además, cuando un microprocesador cambia entre procesos (lo que se denomina un cambio de contexto), es necesario devolver al valor que corresponda para ese proceso, a todos los registros del microprocesador, todas las variables, etc.

Ambas cosas son costosas computacionalmente.

Además se puede dar la situación paradójica de copiar varias veces el mismo código de programa, si tenemos varias instancias o procesos del mismo programa.

Una vez que el modelo de procesos se encontraba maduro y asentado, se buscaron modelos más eficientes. Así, surgieron los denominados ‘procesos ligeros’ (lightweight processes) o ‘threads’ (hilos de ejecución o, simplemente, hilos).

El modelo de threads no contradice el de procesos sino que lo extiende.

Aunque también en el caso de los threads hay variantes e implementaciones diversas, básicamente se trata de aumentar el paralelismo dentro de un proceso. Para ello se pueden realizar ejecuciones paralelas de código dentro de un mismo proceso. Como está dentro de un mismo proceso, se trata del mismo programa, es decir, comparte el código de programa. Esto se aprovecha para que, típicamente, un thread utilice el mismo segmento de programa y el mismo segmento de datos que el proceso y sólo utilice una pila propia. Por ello, la creación y destrucción de threads y los cambios de contexto entre ellos son mucho más baratos computacionalmente que la creación y destrucción de procesos completos o cambios de contexto entre ellos.

Es decir, si la creación de un proceso nuevo implicaba la creación de un segmento de programa ,uno de datos, una pila y un heap, la creación de un thread sólo implica la creación de la pila propia del thread.

A cambio de esta mayor simplicidad y eficiencia, es preciso tomar algunas precauciones en el nivel de programación ya que varios hilos paralelos de ejecución pueden modificar los mismos datos. Es necesario implementar mecanismos de arbitraje y tomar precauciones.

Tendencias
El uso de threads, sin eliminar el concepto de proceso, tiene una implantación imparable ya. Los threads son elementos fundamentales de la programación, por ejemplo, en el lenguaje Java.

Son también elementos imprescindibles en la implementación de servidores web y servidores de aplicación en arquitectura J2EE o .Net. Así, por ejemplo, un servidor web puede ejecutarse en un proceso pero cada solicitud (request) se procesa en un thread diferente.

Conclusiones
Un proceso se puede considerar como un programa en ejecución. Un proceso es manejado por un microprocesador mediante la asignación de unos segmentos de memoria que recogen el espacio para programa, datos, variables locales y argumentos y solicitudes dinámicas de memoria. Los threads aumentan la eficiencia del procesamiento paralelo mediante un modelo más ligero de unidad de ejecución pero imponiendo la toma de precauciones para evitar conflictos en acceso a elementos compartidos del proceso.
LICENCIA
Creative Commons License
Esta obra está bajo una licencia de Creative Commons.