Você está na página 1de 46

Implementación de Hilos

Por lo general, los hilos son proporcionados en la forma de un paquete de hilos. Dicho paquete contiene
operaciones para crear y destruir hilos, así como operaciones con respecto a la sincronización de variables
tales como los mútex y las variables de condición. Existen básicamente dos métodos para implementar un
paquete de hilos. El primer método es la construcción de una biblioteca de hilos que se ejecutan por completo
en el modo de usuario; el segundo es que el kernel esté al tanto de los hilos y los pueda calendarizar.
Una biblioteca de hilos a nivel de usuario tiene algunas ventajas.

Primero, es barato crearla y destruirla. Debido a que toda la administración de los hilos se mantiene en el
espacio de direcciones del usuario, el precio de la creación de un hilo es determinado primordialmente por el
costo de la ubicación de memoria para establecer una pila de hilos. De manera análoga, destruir un hilo
involucra liberar la memoria para la pila, la cual ya no es utilizada. Las dos operaciones son baratas.

Otra ventaja de los hilos a nivel de usuario es que con frecuencia el intercambio de un contexto de hilo puede
realizarse mediante unas cuantas instrucciones. Básicamente, sólo se requiere almacenar los valores de los
registros de la CPU y, posteriormente, recargarla con valores almacenados previamente del hilo al cual se
hace el intercambio. No hay necesidad de modificar los mapas de memoria, el reinicio del TLB, ni hacer un
conteo de la CPU, etc. El intercambio de contextos de hilo se efectúa cuando dos hilos requieren
sincronización, por ejemplo, cuando entramos a una sección de datos compartidos.

Sin embargo, una desventaja importante de los hilos a nivel de usuario es que, al invocar una llamada de
bloqueo de sistema, ésta bloqueará todo el proceso al cual pertenece el hilo, y entonces bloqueará todos los
hilos presentes en dicho proceso. Como ya explicamos, los hilos son particularmente útiles para estructurar
grandes aplicaciones como partes que podemos ejecutar de manera lógica al mismo tiempo. En tal caso, el
bloqueo de E/S pudiera no prevenir a otras partes de ser ejecutadas en la máquina. Para tales aplicaciones, los
hilos a nivel usuario no ayudan.

Por lo general, estos problemas están circunscritos en su mayoría mediante la implementación de hilos dentro
del kernel del sistema operativo. Desafortunadamente, se debe pagar un precio alto: cada operación de un hilo
(creación, eliminación, sincronización, etc.) debe llevarse a cabo por el kernel, lo cual requiere una llamada de
sistema. El intercambio de contextos de hilo puede ser ahora tan costoso como el intercambio de contextos de
proceso. Entonces, como resultado, la mayor parte de los beneficios del uso de hilos en lugar del uso de
procesos desaparece.

Una solución radica en una forma híbrida entre hilos de nivel usuario y nivel kernel, por lo general se le
conoce como procesos de peso ligero (LWP, por sus siglas en inglés). Un LWP se ejecuta en el contexto de
un solo proceso (de peso completo), y puede haber distintos LWP por proceso. Además de contar con LWP,
un sistema ofrece además un paquete de hilos a nivel usuario, lo cual permite a las aplicaciones efectuar las
operaciones necesarias para crear y destruir hilos.

Además, el paquete proporciona los medios para la sincronización de hilos, tales como el mútex y variables
de condición. El asunto importante es que el paquete de hilos está implementado por completo en el espacio
de usuario. En otras palabras, todas las operaciones sobre hilos son procesadas sin la intervención de un
kernel.
Podemos compartir el paquete de hilos mediante múltiples LWP, tal como ilustra la figura 3-2.

Esto significa que cada LWP puede ejecutar su propio hilo (a nivel usuario). Las aplicaciones multihilos se
construyen a partir de la creación de hilos, y por consecuencia se asigna cada hilo a un LWP. Por lo general,
la asignación de un hilo a un LWP está implícita y oculta del programador.

La combinación de (a nivel usuario) hilos y LWP trabaja de la siguiente manera. El paquete de hilos tiene una
rutina simple para calendarizar el siguiente hilo. Al crear un LWP (lo cual se hace por medio de una llamada
de sistema), el LWP cuenta con su propia pila, y tiene la instrucción de ejecutar la rutina de calendarización
en busca de un hilo para ejecución. Si existen varios LWP, entonces cada uno invoca al planificador. La tabla
de hilos, utilizada para seguir la pista del conjunto de hilos actuales, es compartida por los LWP. Al proteger
esta tabla garantizamos que el acceso mutuamente exclusivo se lleve a cabo por medio de los mútex que se
implementan por completo en el espacio de usuario. En otras palabras, la sincronización entre LWP no
requiere soporte alguno del kernel.

Cuando un LWP encuentra un hilo ejecutable, cambia el contexto hacia ese hilo. Mientras tanto, otros LWP
pudieran buscar otros hilos ejecutables. Si un hilo requiere bloquearse mediante un mútex o variable de
condición, lleva a cabo la administración necesaria y, en algún momento, llama a la rutina de planificación.
Cuando otro hilo ejecutable es localizado, se lleva a cabo un intercambio hacia dicho hilo. La belleza de todo
esto es que el LWP que ejecuta el hilo no tiene que estar informado: el intercambio de contexto se implementa
por completo en el espacio de usuario y ante el LWP pasa como un código de programa normal.

Ahora veamos qué sucede cuando un hilo aplica una llamada de bloqueo de sistema. En ese caso, la ejecución
cambia de modo usuario a modo kernel, pero aún así continúa en el contexto del LWP actual. En el punto
donde el actual LWP ya no puede continuar, el sistema operativo pudiera decidir cambiar el contexto hacia
otro LWP, lo cual también implica que un cambio de contexto se regresa al modo usuario. El LWP
seleccionado simplemente continuará en donde se había quedado previamente.

Existen muchas ventajas en el uso de LWP en combinación con el paquete de hilos a nivel del usuario. En
primer lugar, el crear, destruir, y sincronizar hilos es relativamente más barato y no requiere intervención
alguna del kernel. Segundo, en el supuesto de que un proceso no cuente con LWP suficientes, una llamada de
sistema no suspenderá todo el proceso. Tercero, no hay necesidad de que una aplicación sepa algo acerca de
los LWP; todo lo que ve son hilos a nivel del usuario. Cuarto, los LWP pueden utilizarse fácilmente en
ambientes multiproceso, mediante la ejecución de diferentes LWP en diferentes CPU. Se puede ocultar por
completo este multiproceso a la aplicación.

La única desventaja de los procesos ligeros en combinación con hilos de nivel usuario es que aún requieren
crear y destruir LWP, lo cual es tan caro como los hilos a nivel del kernel. Sin embargo, se requiere que la
creación y destrucción de LWP sea sólo ocasional, y por lo general controlado completamente por el sistema
operativo.

Una alternativa, pero similar al método del peso ligero, es hacer uso de las activaciones del calendario
(Anderson y cols., 1991). La diferencia esencial entre las activaciones de calendario y los LWP es que cuando
un hilo bloquea una llamada de sistema, el kernel hace una llamada hacia el paquete de hilos, llamando
efectivamente a la rutina de calendarización para seleccionar el siguiente hilo ejecutable. El mismo
procedimiento se repite cuando el hilo es desbloqueado. La ventaja de este método es que ahorra
administración de los LWP por parte del kernel. Sin embargo, el uso de llamadas es considerado menos
elegante ya que viola la estructura de los sistemas basados en capas, en los cuales solamente son permitidas
las llamadas a la capa inmediata de más bajo nivel.

https://msdn.microsoft.com/es-es/communitydocs/win-
dev/os/manejador-de-procesos-los-threads

http://www.jtech.ua.es/dadm/2011-2012/restringido/android-av/sesion01-apuntes.html

Sistemas Operativos – Manejador de


procesos – Los Threads
Microsoft Community Publishing Service|Última actualización: 26/05/2017

2 Colaboradores



Por Juan Carlos Ruiz Pacheco, Microsoft Senior Technology Evangelist

Network Url

Twitter https://twitter.com/JuanKRuiz

Facebook https://www.facebook.com/JuanKDev

LinkdIn http://www.linkedin.com/in/juankruiz

Blog https://juank.io

Hola, esta es la tercera parte de la serie de Sistemas operativos.

En el post de Manejador de Procesos – Fundamentos identificamos la manera en que


trabaja una CPU y diferenciamos los sistemas monotarea y los sistemas multitarea
donde pudimos ver como hace el sistema para ejecutar varios procesos a la vez. Así
mismo revisamos el tema de lo que es el estado de ejecución de un proceso en un
entorno multitarea.

En el post de Sistemas Operativos – Manejador de procesos – Los Procesos abordamos


los conceptos de proceso, contexto, memoria de trabajo, stack y con conjunto de
instrucciones.

Ahora revisaremos un tema muy importante, hasta ahora todo ha girado entorno a lo
que es un proceso de manera cruda, pero las cosas están a punto de cambiar gracias a
los Threads.

Qué es un Thread y su Uso


Un Thread es un mecanismo que permite a una aplicación realizar varias tareas a la
vez de manera concurrente.

Se les parece al concepto de sistema operativo multitatrea?

Si, es parecido, es la misma filosofia que utiliza el OS para ejecutar varios procesos a la
vez, pero enfocada ejecutar sub procesos de un mismo proceso, lo cual es un poco
diferente ya que por definición los procesos no comparten el espacio de memoria
entre si, mientras que los Threads (o hilos como los quieras llamar) si.
Los Threads son una ampliación del concepto de multitarea, si bien multitarea se
refiere a la capacidad de un sistema para ejecutar varios procesos a la vez, en un
comienzo esto hacia referencia a que más de una aplicación se estuviera ejecutando de
manera concurrente, sin embargo pronto se hizo notoria la necesidad de que una
misma aplicación hiciera varias cosas a la vez. Allí nacieron los Threads.

En un sistema multitarea podemos tener los procesos A, B y C ejecutándose


simultáneamente, pero ¿qué pasaba si el proceso A debía mostrar una interfaz gráfica
y de paso estar escribiendo un archivo a la vez? no era posible.

El proceso debía terminar de escribir en disco antes de volver a trabajar en su interfaz


gráfica lo cual no era precisamente algo deseable. Así que surgió la idea de permitir
que un proceso pueda tener una o mas tareas ejecutándose a la vez o al menos que
así lo percibiera el usuario, de tal forma que cada vez que a un proceso le
correspondiera un Quantum de ejecución el sistema alterne entre ejecutar una de sus
tareas u otra.

Esto conlleva a la necesidad de reestructurar el concepto de proceso, ya que un


proceso no es la unidad mínima de ejecución puesto que ahora el proceso es un
conjunto de tareas (en adelante hilos o threads).

Un proceso que en apariencia no utiliza threads realmente se esta ejecutando en un


único thread.

Consideraciones de los Procesos y los Thread


De acuerdo a lo que acabamos de ver debemos tener en cuenta un par de cosas.

La memoria de trabajo del proceso (de lo cual hablábamos en la parte2) sigue siendo
asignada por proceso, los thread dentro del proceso comparten todos la misma región
de memoria, el mismo espacio de direcciones.

El sistema operativo asigna Quantums de ejecución a cada thread creado, y en efecto


ya no calendariza procesos sino cada uno de los hilos en ejecución en el sistema.

Cada hilo tiene su propio contexto (estado de ejecución), así que cada vez que que se
suspende un hilo para permitir la ejecución de otro, su contexto es guardado y
restablecido nuevamente solo cuando es su turno de ejecución.

Al ser la unidad mínima de ejecución cada thread tiene su propio stack


El proceso si bien ya no es la unidad de ejecución sigue siendo parte fundamental en
el funcionamiento del sistema ya que el proceso hace parte de la asignación de
prioridad de ejecución, es al que se le asigna memoria y de hecho es al que se le
asignan los recursos, así como privilegios y otros datos importantes.

El hilo es solo quien se ejecuta (sin demeritar en absoluto algo tan importante como
esto).

Usar thread no implica necesariamente ejecución en paralelo


Qué sucede si estamos utilizando varios threads en una aplicación que se ejecuta en
una maquina con una sola CPU?

Si bien la impresión del usuario es que se están ejecutando varias cosas al tiempo ya
esta claro que esto no es así pues en la CPU solo se puede ejecutar una cosa a la vez,
lo que esta pasando realmente es que los thread están alternando tiempo de ejecución
de una manera tan rápida que el usuario percibe que se están ejecutando al tiempo.

Pero por otro lado...

Qué sucede si la maquina tiene más de una CPU?

En este caso las cosas pueden cambiar, si nuestra aplicación tiene dos hilos y nuestra
maquina tiene dos CPU (o core) en efecto cada thread se podría ejecutar en una CPU
diferente, en este caso si se puede habar de ejecución en paralelo, aunque no
necesariamente pues puede darse el caso en que, debido a la necesidad del sistema de
calendarizar threads de otros procesos, ambos thread se ejecuten en la misma CPU en
un momento dado, en ese momento no habría paralelismo.

Pero hay otro escenario

Que pasa si mi maquina tiene 2 CPU pero mi aplicación esta utilizando más de 2
thread?

Lo que sucederá es que solo dos de esos thread se estarán ejecutando en paralelo en
un momento dado (aunque ya vimos que esto no es necesariamente lo que sucede), y
el sistema operativo alternara la ejecución de dichos thread de tal forma que todos
tengan Quantums asignados, pero solo podrá haber máximo 2 en paralelo.

¿Es cierto que usar threads hará que mi aplicación se ejecute más rápido?

De acuerdo a lo que vimos en la sección anterior podemos concluir rotundamente


que: DEPENDE.
Como ya vimos si tu maquina tiene solo 1 CPU realmente hará tu aplicación mas lenta,
pero con la ventaja de poder efectuar varias tareas a la vez (en apariencia), pero si
tienes tantas o más CPU como threads en ejecución el rendimiento si que mejorara, es
decir si tienes 2 thread y 2 CPU seguramente que si estarás haciendo dos cosas a la vez
y no una cosa cada vez.

El efecto contrario se evidencia toda vez que trates de ejecutar más threads que las
CPU que tienes, es decir si vas a ejecutar 20 threads y solo tienes 2 CPU en vez de
ganar rendimiento realmente lo que harás será castigarlo puesto que esos thread
estarán compitiendo por el tiempo de CPU, lo cual se traduce en múltiples y
frecuentes cambios de contexto que harán perder el preciado tiempo de CPU en la
lógica necesaria par cambiar de un thread a otro.

En estos escenarios es conveniente administrar la ejecución de los thread para que


solo se ejecuten tantos thread como CPUS existan, y solo entren en ejecución threads
nuevos cuando haya CPUS disponibles. Esto es muy engorroso de hacer pero ya hay
librerias que ayudan a esto como es el caso de TPL y sus derivados a nivel de lenguaje
async/await.

Otra cosa importante de notar es que la creación y la administración de threads es


costosa desde el punto de vista del uso de CPU así que si una aplicación que se ejecuta
en una maquina con más de una CPU, requiere ejecutar una tarea cortada en partes
paralelas probablemente sea mucho mas rápido ejecutarla normalmente que abrirla en
threads, mientras que en una tareas suficientemente grande el tiempo invertido en
crear y administrar los threads puede ser proporcionalmente insignificante.

Diferencias entre Threads del kernel y Threads de Usuario


(Fibras / Fibers).
Si, hay diferentes tipos de thread, dependiendo de la estructura del sistema operativo
esto puede variar.

Pero en términos generales existen los tipos de thread que he mencionado


inicialmente.Veremos como funciona en Windows.

Los thread de Kernel


Todo sistema operativo tiene un kernel, el kernel encargado de todo lo que en esencia
es el sistema operativo ofrece muchas funcionalidades, una de ellas crear threads ya
que son su unidad minina de ejecución y funcionan muy bien tal como lo hemos visto.
En Windows cada vez que se crea un thread se crea un objeto del kernel que tiene
toda la información necesaria respecto a que proceso, cual código ejecutable del
thread, etc. están asignados a dicho thread. Este objeto thread existe en el espacio de
direcciones asignadas al kernel.

Recordemos que cada proceso solo puede acceder a los objetos o áreas de memoria
dentro de su propio espacio de direcciones, entonces Cómo hace un proceso para
acceder a un objeto thread que esta en otro espacio de direcciones ( el del kernel )?
bueno el kernel como tal se encarga de eso asignándole al proceso un manejador al
thread, el kernel mantiene una tabla de que identificadores de recursos tiene asignado
el proceso, así que cuando un proceso quiere acceder a algún objeto del kernel, en
este caso threads, utiliza funciones de la API de Windows que con el identificador del
objeto hacen llamados al kernel los cuales son quienes en ultima instancia manipulan
al objeto en su propio espacio de direcciones del kernel.

Entonces, cada vez que en nuestro proceso utilizamos un thread y queremos modificar
su comportamiento o verificar su información estadística lo que sucede tras
bambalinas es que se hacen llamados al kernel. El kernel proporciona acceso a
funcionalidades que puedes modificar o supervisar el funcionamiento del thread.

El kernel se encarga de manera automática de calendarizar la ejecución de cada uno de


los thread en ejecución. Si un thread de un proceso invoca a un dispositivo de I/O
como por ejemplo la impresora, el thread queda suspendido hasta que la impresora le
conteste pero otros thread del mismo proceso seguirán ejecutándose.

Los thread de usuario


Básicamente son los mismos thread de kernel con la diferencia en que estos no son
administrados por el kernel del sistema operativo, es decir el sistema operativo no
sabe que existen. Son administrados por los programas de usuario.

Un ejemplo de estos son los thread creados en .Net Framework o en java, todos estos
thread son creados, calendarizados y administrados por el runtime de cada uno de
ellos, el sistema operativo en esencia no sabe nada de ellos. Cada thread del kernel
puede tener tener dentro de si uno o mas thread de usuario. El sistema operativo solo
calendariza threads de kernel.

Entonces, cada vez que en nuestro proceso utilizamos un user thread y queremos
modificar su comportamiento o verificar su información estadística lo que sucede tras
bambalinas es que se hacen llamados a funciones dentro del propio espacio de
direcciones del proceso y este se encarga de hacer el trabajo necesario. En adelante
me referiré a los thread de usuario como fiber.
El propio programa se encarga de manera automática de calendarizar la ejecución de
cada uno de los fiber en ejecución.

Si un fiber de un proceso invoca a un dispositivo de I/O como por ejemplo la


impresora, el fiber queda suspendido hasta que la impresora le conteste pero los otros
fiber del mismo proceso también se bloquearan y no seguirán ejecutándose, porque?
recordemos que el sistema operativo no conoce dichos threads así que si uno se
bloquea para el sistema operativo es como si todo el proceso estuviese bloqueado así
que este tipo de thread son totalmente bloqueantes del proceso cuando están a la
espera de respuesta de un dispositivo.

Para solucionar esto, Windows ofrece mecanismos que permiten asociar un fiber a un
thread de kernel nuevo independiente, de tal forma que si por ejemplo el java virtual
machine detecta que uno de sus thread (que son en realidad fibers) queda bloqueado
en espera de un dispositivo, la maquina de java para no bloquear los otros thread del
proceso crea un nuevo thread de sistema operativo y lo asocia con ese fiber para que
el thread principal (donde corren los demas fiber) no quede bloqueado.

Diferencias
Dado lo que he explicado anteriormente podemos contemplar los siguientes aspectos:

Los thread de usuario (fibers) son mucho más eficientes en escenarios con varios
thread que los thread del kernel. Principalmente por dos razones:

1. Los thread de usuario no requieren ser conmutados en modo kernel sino en


modo usuario lo cual permite hacer la conmutación entre threads de manera
más rápida al no tener que alternar de contexto.
2. No son calendarizados de manera preferente, sino que de manera ‘manual’
deben ser suspendidos o reactivados, lo que da la opción de hacer una
calendarización mucho más adecuada de acuerdo al juego de threads que se
estén ejecutando.
Los thread de usuario tienen la desventaja de que no tienen mayor soporte del sistema
operativo lo que conlleva a que hay que hacer mucho trabajo de manera manual, por
ejemplo efectuar la calendarización.

Los thread de usuario bloquean a todos los thread del proceso cuando estos están
bloqueados a espera de una llamada al kernel o a un dispositivo de IO, lo cual hace
que se pierda la funcionalidad de procesamiento paralelo.
Algunos sistemas operativos como es el caso de Windows, proveen funcionalidades
para convertir fiber a kernel thread y viceversa, lo cual facilita dar solución a estos
escenarios de bloqueo.

En términos generales es mucho más recomendable trabajar con Kernel Threads que
con Fibers, dada su mayor complejidad los fibers pueden traer más problemas de lo
que solucionan. Sin embargo hay escenarios donde la implementación de fibers es
muy recomendable y de hecho casi un deber como es en los siguientes casos:

1. migrar una aplicación de Linux/Unix a Windows


2. crear un runtime de ejecución de programas como es el caso del CLR o del java
virtual machine.
3. crear una aplicación profundamente compleja e intensiva a nivel de manejo de
threads
¡Eso es todo!
Hilos de ejecución

En la programación de dispositivos móviles se suele dar el caso de tener que ejecutar una
operación en segundo plano para no entorpecer el uso de la aplicación, produciendo
incómodas esperas. Algunas operaciones, como la descarga de un archivo grande, son
lentas de forma evidente. Otras operaciones que pueden parecer rápidas, a veces también
resultan lentas, si dependen del tamaño de un archivo o de algún factor externo como red.
Los dispositivos continuamente pierden calidad de la señal o pueden cambiar de Wifi a 3G
sin preguntarnos, y perder conexiones o demorarlas durante el proceso. Los hilos también
sirven para ejecutar simultáneamente tareas o para operaciones que se ejecutan con una
periodicidad temporal determinada.

En cuanto a la interfaz gráfica, los hilos son fundamentales para una interacción fluida con
el usuario. Si una aplicación realiza una operación lenta en el mismo hilo de ejecución de la
interfaz gráfica, el lapso de tiempo que dure la conexión, la interfaz gráfica dejará de
responder. Este efecto es indeseable ya que el usuario no lo va a comprender, ni aunque la
operación dure sólo un segundo. Es más, si la congelación dura más de dos segundos, es
muy probable que el sistema operativo muestre el diálogo ANR, "Application not
responding", invitando al usuario a matar la aplicaicón:

Para evitar esto hay que crear otro hilo (Thread) de ejecución que realice la operación
lenta.

Creación y ejecución

Un hilo o Thread es un objeto con un método run(). Hay dos formas de crearlos. Una es
por herencia a partir de Thread y la otra es implementando la interfaz Runnable, que nos
obliga a implementar un método run().

1 public class Hilo1 extends Thread {


2 @Override
public void run() {
3
while(condicion_de_ejecucion){
4 //Realizar operaciones
5 //...
6 try {
7 // Dejar libre la CPU durante
8 // unos milisegundos
Thread.sleep(100);
9 } catch (InterruptedException e) {
10 return;
}
11
}
12 }
13 }
14
15
16
1 public class Hilo2 implements Runnable {
2 @Override
public void run() {
3
while(condicion_de_ejecucion){
4 //Realizar operaciones
5 //...
6 try {
7 // Dejar libre la CPU durante
8 // unos milisegundos
Thread.sleep(100);
9
} catch (InterruptedException e) {
10 return;
11 }
12 }
13 }
14 }
15
16
La diferencia está en la forma de crearlos y ejecutarlos:

1 Hilo1 hilo1 = new Hilo1();


2 hilo1.start();
3
Thread hilo2 = new Thread(new Hilo2());
4
hilo2.start();
5
Una forma todavía más compacta de crear un hilo sería la declaración de la clase en línea:

1 new Thread(new Runnable() {


2 public void run() {
//Realizar operaciones ...
3
}
4 }).start();
5
Si el hilo necesita acceder a datos de la aplicación podemos pasárselos a través del
constructor. Por ejemplo:

1 @Override
2 public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
3
4 setContentView(R.layout.main);
5 new Hilo1(getApplicationContext());
}
6
7
public class Hilo1 extends Thread {
8 Context context;
9 public Hilo2Thread(Context context){
10 this.context = context;
11 this.start();
12 }
@Override
13
public void run() {
14 while(condicion_de_ejecucion){
15 //Realizar operaciones
16 //...
17 try {
18 // Dejar libre la CPU durante
// unos milisegundos
19
Thread.sleep(100);
20 } catch (InterruptedException e) {
21 return;
22 }
23 }
24 }
}
25
26
27
28
En el anterior ejemplo también se ha ejecutado el hilo desde su propio constructor, de
manera que la creación del objeto Hilo1 ha sido suficiente para ejecutarlo.

Ciclo de vida y finalización

Los hilos tienen un ciclo de vida, pasando por diferentes estados:

 New: El primer estado de un hilo recién creado. Permanece en este estado hasta que el hilo
es ejecutado.
 Runnable: Una vez ejecutado pasa a este estado, durante el cuál ejecuta su tarea.
 Not runnable: Estado que permite al hilo desocupar la CPU en espera a que otro hilo
termine o le notifique que puede continuar, o bien a que termine un proceso de E/S, o bien
a que termine una espera provocada por la función Thread.sleep(100);. Tras ello
volverá al estado Runnable.
 Dead: Pasa a este estado una vez finalizado el método run().
El siguiente diagrama resume las transiciones entre los estados del ciclo de vida de un hilo.
Es muy importante asegurar que un hilo saldrá de la función run() cuando sea necesario,
para que pase al estado dead. Por ejemplo, si el hilo ejecuta un bucle while, establecer
a true una variable booleana como condición de que se siga ejecutando. En el momento
que deba terminar su ejecución es suficiente con poner esa variable a false y esperar a
que el hilo de ejecución llegue a la comprobación del bucle while.

No hay un método stop() (está deprecated, por tanto no hay que usarlo), en su lugar el
programador debe programar el mecanismo que termine de forma interna la ejecución del
hilo. Sin embargo si un hilo se encuentra en estado Not Runnable, no podrá hacer
el return de su función run() hasta que no vuelva al estado Runnable. Una forma de
interrumpirlo es usar el método hilo1.interrupt();.

En Android cada aplicación se ejecuta en su propia máquina virtual. Ésta no terminará


mientras haya hilos en ejecución que no sean de tipo daemon. Para hacer que un hilo sea
daemon se utiliza la función:

1 hilo1.setDaemon(true);
2 hilo1.start();

Aún así es recomendable salir de la función run() cuando la lógica del programa lo
considere oportuno.

Hilos y actividades

La información que le haga falta a un hilo se puede pasar por parámetro al construir el
objeto, o bien a posteriori con algún método setter del hilo. Otra alternativa más compacta
sería que la propia actividad implemente Runnable.

1 public class HiloActivity extends Activity


2 implements Runnable{
@Override
3
public void onCreate(Bundle savedInstanceState) {
4 super.onCreate(savedInstanceState);
5 setContentView(R.layout.main);
6 //Iniciar el segundo hilo de ejecución:
7 (new Thread(this)).start();
}
8
9
@Override
10 public void run() {
11 while(condicion_de_ejecucion){
12 //Realizar operaciones
13 //...
14 try {
// Dejar libre la CPU
15
// durante unos milisegundos
16 Thread.sleep(100);
17 } catch (InterruptedException e) {
18 return;
19 }
20 }
}
21
}
22
23
24
25
Nótese que en el ejemplo anterior se podrían crear más hilos de ejecución creando más
instancias del hilo e iniciándolas con (new Thread(this)).start(); pero compartirían
los mismos datos correspondientes a los campos de la clase HiloActivity.

Nota

Al cerrar una Activity los hilos secundarios no terminan. Al pausarla tampoco se pausan. El
programador debe encargarse de eso.

Es importante establecer bien la condición de terminación o de ejecución del hilo, ya que en


una aplicación de Android, al cerrar una Activity, no se finalizan automáticamente los
hilos secundarios de ejecución. De eso se tiene que encargar el programador.

Por defecto para cerrar una actividad hay que pulsar el botón hacia atrás del dispositivo
Android. Sin embargo si se pulsa la tecla Home o se recibe una llamada de teléfono la
actividad pasa a estado pausado. En este caso habría que pausar los threads en el
método onPause() y reestablecerlos en onResume(), con los mecanismos de
sincronización wait y notifyAll. Otra manera aplicable en ciertos casos sería terminar
los hilos y volver a crearlos al salir del estado de pausa de la actividad.

Prioridades

Cada hilo tiene asociada una prioridad que se puede cambiar con el método:

1 hilo1.setPriority(prioridad)

El parámetro es un valor entero entre Thread.MIN_PRIORITY (que vale 1)


y Thread.MAX_PRIORITY (que vale 10). El efecto de la prioridad de los hilos sólo puede
llegar a apreciarse si varios hilos están simultáneamente en estado "Running". Si el hilo
tiene estados "Waiting", o bien para liberar CPU, o bien porque esperan E/S, o bien porque
esperan otro hilo, en estos intervalos de tiempo el procesador estaría liberado para ejecutar
el otro hilo, independientemente de su prioridad.

Sincronización de hilos

Acceso a datos compartidos

Cuando varios hilos pueden acceder simultáneamente a algún dato, hay que asegurarse de
que no lo hagan simultáneamente en el caso de estar modificándolo. Para ello utilizaremos
el modificador synchronized sobre métodos:

1 public synchronized void incrementa(int incremento){


2 this.valor += incremento;
}
3
De esta manera si un hilo empieza a ejecutar este método, todos los demás hilos que
intenten ejecutarlo tendrán que esperar a que el primero que entró salga del método.

Una manera alternativa de escribirlo hubiera sido utilizando de manera explícita la


variable lock del objeto:

1 public synchronized void incrementa(int incremento){


2 synchronized(this){
this.valor += incremento;
3
}
4 }
5
Hay estructuras de datos cuyos métodos ya están sincronizados, por ejemplo Vector que
es la versión sincronizada de List. Sin embargo hay que llevar cuidado si las operaciones
que realizamos no son atómicas, como por ejemplo obtener un valor, tratarlo, y después
modificarlo. Si varios hilos acceden a esta secuencia de código, hay que sincronizarla para
evitar llegar a estados inconsistentes de los datos.

Cuando se accede a otro tipo de recursos, como a un archivo en disco, también es


fundamental asegurar la sincronización. Sin embargo si la sección crítica va a ser muy
grande hay que tener en cuenta que esto puede disminuir el rendimiento. En general no
hay que sincronizar si no es necesario.

Esperar a otro hilo

En el acceso a datos compartidos se ha visto cómo hacer que los demás hilos esperen a
que otro salga de una sección crítica. Java también proporciona en la
clase Thread métodos que permiten bloquear un hilo de manera explícita y desbloquearlo
cuando sea necesario, son los métodos wait() y notify() (o notifyAll()).

Para evitar interbloqueos ambos métodos deben ser llamados siempre desde secciones de
código sincronizadas. En el siguiente ejemplo una estructura de datos está preparada para
ser accedida simultáneamente por varios hilos productores y consumidores. Es capaz de
almacenar un único objeto pero si un consumidor accede y la estructura no contiene ningún
objeto, dicho consumidor queda bloqueado hasta que algún productor introduzca un objeto.
Al introducirlo, desbloqueará de manera explícita al hilo consumidor. Durante dicho bloqueo
la CPU está completamente libre del hilo consumidor.

1 public class CubbyHole {


2 private Object cubby;
3
//El productor llamaría a este método.
4
public synchronized Object produce(Object data) {
5 Object ret = cubby;
6 cubby = data;
7
8 // Desbloquear el consumidor que
9 // esté esperando en consume().
10 notifyAll();
11
return ret;
12
}
13
14 //El consumidor llamaría a este método
15 public synchronized Object consume()
16 throws InterruptedException {
17 // Bloquear hasta que exista
18 // un objeto que consumir
while (cubby == null) {
19
// Libera el lock del
20 // objeto mientras espera y
21 // lo cierra al seguir ejecutándose
22 wait();
23 }
24
25 Object ret = cubby;
cubby = null;
26
27
return ret;
28 }
29 }
30
31
32
33
Otra forma de sincronización es la de esperar a que un hilo termine para continuar con la
ejecución del presente hilo. Para este propósito se utiliza el método join(Thread):

1 Thread descarga1 = new Thread(new DescargaRunnable(url1));


2 Thread descarga2 = new Thread(new DescargaRunnable(url2));
//Bloquear hasta que ambas se descarguen
3
descarga1.join();
4 descarga2.join();
5 //Descargadas, realizar alguna operación con los datos
6
Pausar un hilo
En las aplicaciones de Android los hilos no se pausan automáticamente cuando
la Activity pasa a estado de pausa. Puede ser adecuado pausarlos para ahorrar recursos
y porque el usuario suele asumir que una aplicación que no esté en primer plano, no
consume CPU.

Igual que para detener un hilo suele ser conveniente tener una variable booleana que
indique esta intención, en el siguiente ejemplo se añade una variable paused para indicar
que el hilo debe ser bloqueado. La llamada a wait() bloquea el hilo utilizando el lock de un
objeto pauseLock. Cuando se debe reanudar el hilo, se realiza una llamada
a notifyAll(). Ambas llamadas están en una secciónsynchronized.

1 class MiRunnable implements Runnable {


2 private Object pauseLock;
private boolean paused;
3
private boolean finished;
4
5 public MiRunnable() {
6 pauseLock = new Object();
7 paused = false;
8 finished = false;
9 }
10
public void run() {
11
while (!finished) {
12 // Realizar operaciones
13 // ...
14 synchronized (pauseLock) {
15 while (paused) {
16 try {
pauseLock.wait();
17
} catch (
18 InterruptedException e) {
19 break;
20 }
21 }
22 }
}
23
}
24 //Llamarlo desde Activity.onPause()
25 public void pause() {
26 synchronized (pauseLock) {
27 paused = true;
28 }
}
29
//Llamarlo desde Activity.onResume()
30 public void resume() {
31 synchronized (pauseLock) {
32 paused = false;
33 pauseLock.notifyAll();
}
34 }
35 //Llamarlo desde Activity.onDestroy()
public void finish() {
36
finished = true;
37 }
38 }
39
40
41
42
43
44
45
Hay que prestar atención a que el método Activity.onResume() no sólo es llamado tras
un estado de pausa sino también al crear la actividad por primera vez. Por tanto habrá que
comprobar que el hilo no sea null, antes de hacer la llamada a MiRunnable.resume().

Mecanismos específicos en Android

Android cuenta con un framework de mensajería y concurrencia que está basado en las
clases Thread, Looper, Message, MessageQueue y Handler. Por conveniencia existe
también la clase AsyncTask, para usos comunes de actualización de la UI. Un Looper se
utiliza para ejecutar un bucle de comunicación por mensajes en determinado hilo. La
interacción con el Looper se realiza a través de la clase Handler, que permite enviar y
procesar un Message y objetos que implementen Runnable, asociados a una
cola MessageQueue.

Mensajes mediante Handlers


Handler permite enviar y procesar un Message y objetos que implementen Runnable.
Cada instancia de Handler se asocia con un único hilo y su cola de mensajes. Hay que
crear el Handler desde el mismo hilo al que debe ir asociado. Un hilo puede tener asociados
varios Handler.

Mediante el uso de Handler se pueden encolar una acciones que que deben ser realizadas
en otro hilo de ejecución distinto. Esto permite programar acciones que deben ser
ejecutadas en el futuro, en un orden determinado.

El envío se realiza mediante post(Runnable) o postDelayed(Runnable, long),


donde se indican los milisegundos de retardo para realizar el post.

1 public class Actividad extends Activity {


2 TextView textView;
Handler handler;
3
4
@Override
5 public void onCreate(Bundle savedInstanceState) {
6 super.onCreate(savedInstanceState);
7 setContentView(R.layout.main);
8 textView = (TextView) findViewById(R.id.textview1);
9
10 handler = new Handler();
11 handler.removeCallbacks(tarea1);
handler.removeCallbacks(tarea2);
12
handler.post(tarea1);
13 handler.post(tarea2);
14 }
15
16 private Runnable tarea1 = new Runnable() {
17 @Override
18 public void run() {
textView.setText("Primer cambio");
19
}
20 };
21
22 private Runnable tarea2 = new Runnable() {
23 @Override
24 public void run() {
25 textView.setText("Segundo cambio");
}
26
};
27
28 }
29
30
31
32
En el ejemplo anterior las tareas se ejecutarían por orden y lo harían en el mismo hilo de
ejecución. El resultado visible sería el del segundo cambio.

En el siguiente ejemplo se realiza una carga lenta de datos en otro hilo, al tiempo que se
muestra una splash screen para indicar que se están cargando los datos. Una vez finalizada
la tarea se envía un mensaje vacío. Este mensaje se captura con un Handler propio y se
realiza el cambio de vistas de la Actividad.

Ejemplo: pantalla de inicialización


1 public class Actividad extends Activity implements Runnable{
2 @Override
public void onCreate(Bundle savedInstanceState) {
3
super.onCreate(savedInstanceState);
4 //debe estar definido en /res/layout:
5 setContentView(R.layout.splashscreen);
6
7 (new Thread(this)).start();
8 }
9 private Handler handler = new Handler(){
public void handleMessage(Message msg){
10
setContentView(R.layout.main);
11 }
12 };
13 @Override
14 public void run() {
//Carga lenta de datos...
15
try {
16 Thread.sleep(2000);
17 } catch (InterruptedException e) {
18 }
19 handler.sendEmptyMessage(0);
20 }
}
21
22
23
24

Hilos con Looper


Looper se utiliza para ejecutar un bucle de comunicación por mensajes en determinado
hilo. Por defecto los hilos no tienen ningún bucle de mensajes asociado. Para crearlo hay
que llamar a Looper.prepare()desde el hilo correspondiente. En el siguiente ejemplo se
implementa un hilo que cuenta con un método post(Runnable) para encolar las tareas
al handler.
1 class MiHiloLooper extends Thread{
2 Handler handler; //message handler
3
public MiHiloLooper(){
4
this.start();
5 }
6
7 @Override
8 public void run(){
9 try{
10 Looper.prepare();
handler = new Handler();
11
Looper.loop();
12 }catch(Throwable t){
13 Log.e("Looper","Error: ", t);
14 }
15 }
16
17 public void terminate(){
handler.getLooper().quit();
18
}
19
20 public void post(Runnable runnable){
21 handler.post(runnable);
22 }
23 }
24
25
26
Se usaría declarándolo y haciendo pasando a post() objetos Runnable cada vez que sea
necesario. En el ejemplo siguiente no sería estrictamente necesario utilizar un looper, pero
se puede pensar en un caso en el que no se sabe a priori qué Runnables y cuándo habrá
que ejecutar.

1 public class Actividad extends Activity {


2 MiHiloLooper looper;
ImageView iv1, iv2, iv3;
3
URL url1, url2, url3;
4
5 @Override
6 public void onCreate(Bundle savedInstanceState) {
7 //... Inicializar views ...
8 //... Inicializar urls ...
9
10 looper = new MiHiloLooper();
looper.post(new Thread( new ImageLoader(url1, iv1) ));
11
looper.post(new Thread( new ImageLoader(url2, iv2) ));
12 looper.post(new Thread( new ImageLoader(url3, iv3) ));
13
14 }
15
16 @Override
protected void onDestroy() {
17
looper.terminate();
18 super.onDestroy();
19 }
20 }
21
22
23
Las tres imágenes se cargarían una detrás de otra (y no en paralelo) en el orden
establecido. La clase ImageLoader sería un Runnable que tendría que descargar la imagen
y ponerla en su ImageView. El acceso a la interfaz podría realizarse a través del
método ImageView.post(), ya que en el anterior ejemplo no hemos declarado ningún
Handler. Por ejemplo podría quedar así:

1 class ImageLoader implements Runnable{


2 ImageView iv;
URL url;
3
4
public ImageLoader(String url, ImageView iv){
5 this.iv = iv;
6 this.url = url;
7 }
8
9 @Override
10 public void run() {
try {
11
InputStream is = url.openStream();
12 final Drawable drawable =
13 Drawable.createFromStream(is, "src");
14 button.post(new Runnable() {
15 @Override
16 public void run() {
iv.setImageDrawable(drawable);
17
}
18 });
19 } catch (IOException e) {
20 Log.e("URL","Error downloading image "+
21 url.toString());
22 } catch (InterruptedException e) {
}
23
}
24 }
25
26
27
28
Pools de hilos

Los pools de hilos permiten no sólo encolar tareas a ejecutar, sino también controlar
cuántas ejecutar simultáneamente. Por ejemplo, si se quieren cargar decenas de imágenes,
pero máximo de dos en dos.

1 public class AvHilos8PoolExecutorActivity extends Activity {


2 int poolSize = 2;
3 int maxPoolSize = 2;
4 long keepAliveTime = 3;
5
ThreadPoolExecutor pool;
6
ArrayBlockingQueue<Runnable> queue;
7
8 @Override
9 public void onCreate(Bundle savedInstanceState) {
10 super.onCreate(savedInstanceState);
11 setContentView(R.layout.main);
12
13 queue = new ArrayBlockingQueue<Runnable>(5);
pool = new ThreadPoolExecutor(
14
poolSize, maxPoolSize, keepAliveTime,
15 TimeUnit.SECONDS, queue);
16 pool.execute(new MyRunnable("A"));
17 pool.execute(new MyRunnable("B"));
18 pool.execute(new MyRunnable("C"));
19 pool.execute(new MyRunnable("D"));
pool.execute(new MyRunnable("E"));
20
}
21 }
22
23
24
Los parámetros que se pasan a ThreadPoolExecutor al crearlo son el número mínimo de
hilos a mantener en el pool, incluso aunque no estén en ejecución, el número máximo de
hilos permitido, el tiempo que se permite que permanezca un hilo que ya no esté en
ejecución, la unidad de tiempo para el anterior argumento y por último la cola de tareas.

Cada ThreadPoolExecutor guarda estadísticas básicas, como por ejemplo el número de


tareas completadas. Se pueden obtener a través de los métodos del pool y de la cola:

1 queue.size() );
2 pool.getActiveCount() );
pool.getTaskCount() );
3

Hilos e interfaz de usuario

Una de las principales motivaciones para el uso de hilos es permitir que la interfaz gráfica
funcione de forma fluida mientras se están realizando otras operaciones. Cuando una
operación termina, a menudo hay que reflejar el resultado de la operación en la interfaz de
usuario (UI, de user interface). Otro caso muy común es ir mostrando un progreso de una
operación lenta.

Considérese el siguiente ejemplo en el cuál se descarga una imagen desde una URL y al
finalizar la descarga, hay que mostrarla en un imageView.

1 ImageView imageView =
2 (ImageView)findViewById(R.id.ImageView01);
new Thread(new Runnable() {
3
public void run() {
4 Drawable imagen = descargarImagen("http://...");
5 //Desde aquí NO debo acceder a imageView
6 //imageView.setImageDrawable(imagen)
7 //daría error en ejecución
8 }
}).start();
9
10
Tras cargar la imagen no podemos acceder a la interfaz gráfica porque la GUI de Android
sigue un modelo de hilo único: sólo un hilo puede acceder a ella. Se puede solventar de
varias maneras:

 Activity.runOnUiThread(Runnable)
 View.post(Runnable)
 View.postDelayed(Runnable, long)
 Handler
 AsyncTask
El método Activity.runOnUiThread(Runnable) es similar a View.post(Runnable).
Ambos permitirían que el Runnable que pasamos por parámetro se ejecute en el mismo
hilo que la UI.

1 ImageView imageView =
2 (ImageView)findViewById(R.id.ImageView01);
new Thread(new Runnable() {
3
public void run() {
4 Drawable imagen = descargarLaImagen("http://...");
5 imageView.post(new Runnable() {
6 public void run() {
7 imageView.setDrawable(imagen);
8 }
});
9
}
10 }).start();
11
12
Otra manera sería por medio de Handlers, con el mismo fin: que la actualización de la UI se
ejecute en su mismo hilo de ejecución. El Handler se declara en el hilo de la UI (en la
actividad) y de manera implícita se asocia al Looper que contiene el hilo de la UI. Después
en el hilo secundario se realiza un post a dicho Handler.
1 public class Actividad extends Activity {
2 private Handler handler;
private ImageView imageView;
3
4
@Override
5 public void onCreate(Bundle savedInstanceState) {
6 // ... Inicializar views ...
7
8 handler = new Handler();
9
10 Runnable runnable = new Runnable() {
11 @Override
public void run() {
12
Drawable imagen =
13 descargarLaImagen("http://...");
14 handler.post(new Runnable() {
15 @Override
16 public void run() {
17 imageView.setDrawable(imagen);
}
18
});
19 }
20 };
21 new Thread(runnable).start();
22 }
23 }
24
25
26

AsyncTask

Otra manera es utilizar una AsyncTask. Es una clase creada para facilitar el trabajo con
hilos y con interfaz gráfica, y es muy útil para ir mostrando el progreso de una tarea larga,
durante el desarrollo de ésta. Nos facilita la separación entre tarea secundaria e interfaz
gráfica permitiéndonos solicitar un refresco del progreso desde la tarea secundaria, pero
realizarlo en el hilo principal.

1 TextView textView;
2 ImageView[] imageView;
3
public void bajarImagenes(){
4
textView = (TextView)findViewById(R.id.TextView01);
5 imageView[0] = (ImageView)findViewById(R.id.ImageView01);
6 imageView[1] = (ImageView)findViewById(R.id.ImageView02);
7 imageView[2] = (ImageView)findViewById(R.id.ImageView03);
8 imageView[3] = (ImageView)findViewById(R.id.ImageView04);
9
10 new BajarImagenesTask().execute(
"http://a.com/1.png",
11 "http://a.com/2.png",
12 "http://a.com/3.png",
"http://a.com/4.png");
13
}
14 private class BajarImagenesTask
15 extends AsyncTask<String, Integer, List<Drawable>> {
16 @Override
17 protected List<Drawable> doInBackground(String... urls) {
18 ArrayList<Drawable> imagenes =
new ArrayList<Drawable>();
19
for(int i=1;i<urls.length; i++){
20 cargarLaImagen(urls[0]);
21 <strong>publishProgress(i);</strong>
22 return imagenes;
23 }
24
25 @Override
protected void onPreExecute() {
26
super.onPreExecute();
27 textView.setText("Cargando imagenes...");
28 }
29
30 @Override
31 protected void <strong>onProgressUpdate(String... values)</strong> {
32 textView.setText(values[0] +
" imagenes cargadas...");
33
}
34
35 @Override
36 protected void onPostExecute(List<Drawable> result) {
37 for(int i=0; i<result.length; i++){
38 imageView[i].setDrawable(result.getItemAt(i));
39 }
textView.setText("Descarga finalizada");
40
}
41
42 @Override
43 protected void onCancelled() {
44 textView.setText("Cancelada la descarga");
45 }
46 }
47
48
49
50
51
52
53
Nota
La notación (String ... values) indica que hay un número indeterminado de parámetros, y se
accede a ellos con values[0], values[1], ..., etcétera. Forma parte de la sintaxis estándar de
Java.

Lo único que se ejecuta en el segundo hilo de ejecución es el bucle del


método doInBackground(String...). El resto de métodos se ejecutan en el mismo hilo
que la interfaz gráfica. La petición de publicación de
progreso, publishProgress(...) está resaltada, así como la implementación de la
publicación del progreso, onProgressUpdate(...). Es importante entender que la
ejecución de onProgressUpdate(...) no tiene por qué ocurrir inmediatamente después
de la petición publishProgress(...), o puede incluso no llegar a ocurrir.

El uso de AsyncTask es una práctica recomendable en Android porque el


método post() puede hacer el código menos legible. Esta estructura está en Android
desde la versión 1.5.
Procesos ligeros: disección de hilos de
Linux
VISHAL KANAUJIA , 1 DE AGOSTO DE 2011

8.52K 5

Este artículo, dirigido a desarrolladores de Linux y estudiantes de informática, explora los


fundamentos de los hilos y su implementación en Linux con procesos de peso liviano, lo que
ayuda a comprender la implementación de un código.

Los hilos son el elemento central de un entorno de programación multitarea. Por definición, un
hilo es un contexto de ejecución en un proceso; por lo tanto, cada proceso tiene al menos un
hilo. Multi-threading implica la existencia de contextos de ejecución múltiples, concurrentes
(en sistemas multiprocesador) y, a menudo sincronizados en un proceso.

Los subprocesos tienen su propia identidad (ID de subproceso) y pueden funcionar de forma
independiente. Comparten el espacio de direcciones dentro del proceso y aprovechan los
beneficios de evitar cualquier canal de IPC (comunicación entre procesos) (memoria
compartida, tuberías, etc.) para comunicarse. Los hilos de un proceso pueden comunicarse
directamente entre sí, por ejemplo, los hilos independientes pueden acceder / actualizar una
variable global. Este modelo elimina la sobrecarga potencial de IPC en la que el kernel debería
haber incurrido. Como los hilos están en el mismo espacio de direcciones, un cambio de
contexto de hilo es barato y rápido.

Un hilo se puede programar de forma independiente; por lo tanto, las aplicaciones de


subprocesos múltiples son adecuadas para explotar el paralelismo en un entorno
multiprocesador. Además, la creación y destrucción de hilos es rápida. A diferencia fork(), no
hay una copia nueva del proceso principal, pero usa el mismo espacio de direcciones y
comparte recursos, incluidos los descriptores de archivos y los manejadores de señales.

Una aplicación multiproceso utiliza los recursos de manera óptima y es altamente eficiente. En
dicha aplicación, los subprocesos se cargan con diferentes categorías de trabajo, de tal manera
que el sistema se usa de manera óptima. Un hilo puede estar leyendo un archivo del disco y otro
escribiéndolo en un socket. Ambos trabajan en tándem, pero son independientes. Esto mejora la
utilización del sistema y, por lo tanto, el rendimiento.
Algunas preocupaciones
La preocupación más importante con los hilos es la sincronización, especialmente si hay un
recurso compartido, marcado como una sección crítica. Este es un fragmento de código que
accede a un recurso compartido y no debe accederse simultáneamente por más de un hilo. Dado
que cada subproceso se puede ejecutar de forma independiente, el acceso al recurso compartido
no se modera de forma natural, sino que utiliza primitivas de sincronización que incluyen
mutexes (exclusión mutua), semáforos, bloqueos de lectura / escritura, etc.

Estas primitivas permiten a los programadores controlar el acceso a un recurso


compartido. Además, al igual que los procesos, los hilos también sufren estados de punto
muerto o inanición, si no se diseñan cuidadosamente. Depurar y analizar una aplicación de
subprocesos también puede ser un poco engorroso.

¿Cómo implementa Linux los hilos?


Linux admite el desarrollo y la ejecución de aplicaciones multihilo. Los subprocesos de nivel
de usuario en Linux siguen el estándar abierto POSIX (Interfaz de sistema operativo portátil
para uniX), designado como IEEE 1003. La biblioteca de nivel de usuario (en
Ubuntu glibc.so) tiene una implementación de la API POSIX para subprocesos.

Los hilos existen en dos espacios de ejecución separados en Linux: en el espacio de usuario y
en el kernel. Los subprocesos del espacio de usuario se crean con la pthreadAPI
de la biblioteca (compatible con POSIX). Estos subprocesos de espacio de usuario se asignan a
hilos de kernel. En Linux, los hilos del kernel son considerados como "procesos livianos". Un
LWP es la unidad de un contexto de ejecución básico. A diferencia de otras variantes de UNIX,
como HP-UX y SunOS, no existe un tratamiento especial para los hilos. Un proceso o un hilo
en Linux se trata como una "tarea", y comparte la misma representación de estructura (lista
de struct task_structs).

Para un conjunto de hilos de usuario creados en un proceso de usuario, hay un conjunto de


LWP correspondientes en el kernel. El siguiente ejemplo ilustra este punto:

#include <stdio.h>

#include <syscall.h>

#include <pthread.h>

int main()

pthread_t tid = pthread_self();


int sid = syscall(SYS_gettid);

printf("LWP id is %dn", sid);

printf("POSIX thread id is %dn", tid);

return 0;

Al ejecutar el pscomando también, se enumeran los procesos y su información de LWP / hilos:

kanaujia@ubuntu:~/Desktop$ ps -fL

UID PID PPID LWP C NLWP STIME TTY TIME CMD

kanaujia 17281 5191 17281 0 1 Jun11 pts/2 00:00:02 bash

kanaujia 22838 17281 22838 0 1 08:47 pts/2 00:00:00 ps -fL

kanaujia 17647 14111 17647 0 2 00:06 pts/0 00:00:00 vi clone.s

¿Qué es un proceso ligero?


Un LWP es un proceso creado para facilitar un hilo de espacio de usuario. Cada subproceso de
usuario tiene una asignación de 1 × 1 a un LWP. La creación de LWPs es diferente de un
proceso ordinario; para un proceso de usuario "P", su conjunto de LWP comparte la misma ID
de grupo. Agruparlos permite que el kernel habilite el uso compartido de recursos entre ellos
(los recursos incluyen el espacio de direcciones, las páginas de memoria física (VM), los
manejadores de señal y los archivos). Esto permite además que el núcleo evite los cambios de
contexto entre estos procesos. El amplio uso compartido de recursos es la razón por la cual
estos procesos se denominan procesos ligeros.

¿Cómo crea Linux LWP?


Linux maneja LWPs a través de la clone()llamada al sistema no estándar . Es similar
a fork(), pero más genérico. En realidad, fork()es una manifestación de clone(), que permite
a los programadores elegir los recursos para compartir entre los procesos. La clone()llamada
crea un proceso, pero el proceso hijo comparte su contexto de ejecución con el padre, incluida
la memoria, los descriptores de archivo y los controladores de señal. La pthreadbiblioteca
también usa clone()para implementar
subprocesos. Consulte ./nptl/sysdeps/pthread/createthread.cen las fuentes glibc versión
2.11.2.

Crea tu propio LWP


Demostraré un uso de muestra de la clone()llamada. Eche un vistazo al código
a demo.ccontinuación:
1 #include <malloc.h>

2 #include <sys/types.h>

3 #include <sys/wait.h>

#include <signal.h>
4
#include <sched.h>
5
#include <stdio.h>
6
#include <fcntl.h>
7

8
// 64kB stack
9
#define STACK 1024*64
10

11 // The child thread will execute this function

12 int threadFunction( void* argument ) {

13 printf( "child thread entering\n" );

14 close((int*)argument);

printf( "child thread exiting\n" );


15
return 0;
16
}
17

18
int main() {
19
void* stack;
20
pid_t pid;
21 int fd;

22

23 fd = open("/dev/null", O_RDWR);

24 if (fd < 0) {

25 perror("/dev/null");
26 exit(1);

27 }

28
// Allocate the stack
29
stack = malloc(STACK);
30
if (stack == 0) {
31
perror("malloc: could not allocate stack");
32
exit(1);
33
}
34 printf("Creating child thread\n");

35

36 // Call the clone system call to create the child thread

37 pid = clone(&threadFunction,

38 (char*) stack + STACK,

SIGCHLD | CLONE_FS | CLONE_FILES |\


39
CLONE_SIGHAND | CLONE_VM,
40
(void*)fd);
41

42
if (pid == -1) {
43
perror("clone");
44
exit(2);
45 }

46

47 // Wait for the child thread to exit

48 pid = waitpid(pid, 0, 0);

49 if (pid == -1) {

50 perror("waitpid");
51 exit(3);

52 }

53
// Attempt to write to file should fail, since our thread has
54
// closed the file.
55
if (write(fd, "c", 1) < 0) {
56
printf("Parent:\t child closed our file descriptor\n");
57
}
58

59
// Free the stack
60 free(stack);

61

62 return 0;

63 }

64

65

66

67

68

El programa en demo.cpermite la creación de hilos, y es fundamentalmente similar a lo que


hace la pthreadbiblioteca. Sin embargo, clone()se desaconseja su uso directo , ya que si no se
usa correctamente, puede bloquear la aplicación desarrollada. La sintaxis para
llamar clone()en un programa Linux es la siguiente:

#include <sched.h>

int clone (int (*fn) (void *), void *child_stack, int flags, void *arg);

El primer argumento es la función hilo; se ejecutará una vez que comience un


hilo. Cuando clone()se complete con éxito, fnse ejecutará simultáneamente con el proceso de
llamada.
El siguiente argumento es un puntero a una memoria de pila para el proceso secundario. Un
paso atrás de fork(), clone()exige que el programador asigne y establezca la pila para el
proceso secundario, porque el padre y el hijo comparten páginas de memoria, y eso incluye
también la pila. El niño puede elegir llamar a una función diferente a la del padre, por lo tanto,
necesita una pila separada. En nuestro programa, asignamos este fragmento de memoria en el
montón, con la malloc()rutina. El tamaño de la pila se ha establecido en 64 KB. Dado que la
pila en la arquitectura x86 crece hacia abajo, necesitamos simularla utilizando la memoria
asignada desde el otro extremo. Por lo tanto, pasamos la siguiente dirección a clone():

(char*) stack + STACK

El siguiente campo, flagses el más crítico. Le permite elegir los recursos que desea compartir
con el proceso recién creado. Hemos elegido SIGCHLD | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_VM, que se explica a continuación:

 SIGCHLD: El hilo envía una SIGCHLDseñal al proceso principal después de la


finalización. Le permite al padre wait()completar todos sus subprocesos.
 CLONE_FS: Comparte la información del sistema de archivos del padre con su hilo. Esto
incluye la raíz del sistema de archivos, el directorio de trabajo actual y umask.
 CLONE_FILES: El proceso de llamada y llamante comparten la misma tabla de descriptor
de archivo. Cualquier cambio en la tabla se refleja en el proceso padre y todos sus hilos.
 CLONE_SIGHAND: Parent y threads comparten la misma tabla de manejador de señal. De
nuevo, si el padre o cualquier hilo modifica una acción de señal, se refleja a ambas
partes.
 CLONE_VM: El padre y los subprocesos se ejecutan en el mismo espacio de
memoria. Cualquier escritura / mapeo de memoria realizada por cualquiera de ellos es
visible para otros procesos.

El último parámetro es el argumento de la función thread ( threadFunction), y es un descriptor


de archivo en nuestro caso.

Consulte la implementación del código de muestra de LWP, en la demo.cpresentación anterior.

El hilo cierra el archivo ( /dev/null) abierto por el padre. Como el padre y este subproceso
comparten la tabla de descriptores de archivos, la operación de cierre del archivo también se
reflejará en el contexto write()primario y fallará una operación de archivo posterior en el
elemento principal. El padre espera hasta que finalice la ejecución del hilo (hasta que reciba
a SIGCHLD). Luego, libera la memoria y regresa.

Compila y ejecuta el código como de costumbre; y debería ser similar a lo que se muestra a
continuación:

$gcc demo.c

$./a.out

Creating child thread


child thread entering

child thread exiting

Parent: child closed our file descriptor

Linux proporciona soporte para una infraestructura eficiente, simple y escalable para
subprocesos. Alienta a los programadores a experimentar y desarrollar bibliotecas de hilos
utilizando clone()como componente central.

Por favor, comparta sus sugerencias / comentarios en las secciones de comentarios a


continuación.

Referencias y lectura sugerida

1. Artículo de Wikipedia sobre clone()


2. página man de clone ()
3. Usando el clon () System Call por Joey Bernard
4. Implementación de una biblioteca de subprocesos en Linux
5. Interpretaciones de estándares IEEE para IEEE Std 1003.1c-1995
6. Fuentes de implementación pthread en glibc.so
7. Las fibras de hilos de Benjamin Chelf

Cortesía de la imagen de la función: foto.bulle . Reutilizado bajo los términos de la licencia


CC-BY 2.0.

https://es.slideshare.net/AlejandroCalderonMat/diseo-de-sistemas-operativos-procesos-hilos-y-
planificacin-v2b
Arquitectura Virtualización
Existen diferentes formas mediante las cuales podemos ejecutar la virtualización. En el libro de Smith y Nair
(2005) podemos ver una descripción de tales métodos. Para comprender las diferencias en la virtualización, es
importante darse cuenta de que, por lo general, los sistemas de cómputo ofrecen cuatro tipos de interfaz
distintos, y en cuatro niveles diferentes:

1. Una interfaz entre el hardware y el software, constituida por instrucciones máquina que se pueden invocar
desde cualquier programa.

2. Una interfaz entre el hardware y el software, constituida por instrucciones máquina que se pueden invocar
solamente desde programas privilegiados, tales como los sistemas operativos.

3. Una interfaz que consta de llamadas de sistema como las que ofrece un sistema operativo.

4. Una interfaz que consta de llamadas a bibliotecas, las cuales forman, por lo general, lo que conocemos
como interfaz de programación de aplicaciones (API, por sus siglas en inglés). En muchos casos, las
llamadas de sistema ya mencionadas están ocultas por una API.

En la figura 3-6 mostramos los diferentes tipos de virtualización. La esencia de la virtualización es imitar el
comportamiento de estas interfaces.
La virtualización puede tener lugar en dos formas diferentes. Primero, podemos construir un sistema en
tiempo de ejecución que esencialmente proporcione un conjunto de instrucciones para ser utilizado en la
ejecución de aplicaciones. Las instrucciones se pueden interpretar (como en el caso del ambiente en tiempo de
ejecución de Java JRE, por sus siglas en inglés), pero pudieran ser emuladas también como se hace en
aplicaciones que se ejecutan en Windows sobre plataformas UNIX. Observe que en el caso referido a
continuación, el emulador tendrá que imitar el comportamiento de las llamadas de sistema, las cuales han
mostrado ir más allá de lo trivial.

Este tipo de virtualización nos lleva a lo que Smith y Nair (2005) llaman una máquina virtual de proceso, lo
cual enfatiza que la virtualización se implementa esencialmente solamente para un proceso.

Un método alternativo hacia la virtualización es el que proporciona un sistema que básicamente se


implementa como una capa que cubre por completo al hardware original, pero que ofrece todo un conjunto de
instrucciones del mismo (o de otro hardware) como una interfaz. Resulta crucial el hecho de que se puede
ofrecer esta interfaz de manera simultánea a diferentes programas. Como resultado, ahora es posible tener
múltiples y diferentes sistemas operativos que se ejecutan de distinto modo y concurrentemente sobre la
misma plataforma. A esta capa, por lo general, la conocemos como el monitor de la máquina virtual
(VMM, por sus siglas en inglés). Ejemplos típicos de este enfoque son VMware (Sugerman y cols., 2001) y
Xen (Barham y cols., 2003). En la figura 3-7 se muestran estos dos métodos.

Tal como argumentan Rosenblum y Garfinkel (2005), las VMM serán cada vez más importantes en el
contexto de la confiabilidad y la seguridad para los sistemas (distribuidos). Dado que permiten el aislamiento
de toda una aplicación y de su ambiente, una falla ocasionada por un error o un ataque a la seguridad no
afectará a una máquina en su totalidad. Además, como ya mencionamos, la portabilidad mejora de manera
importante dado que las VMM proporcionan un desacoplamiento posterior entre hardware y software, lo cual
permite mover un ambiente completo desde una máquina a otra.
Maquina Virtual y Modelo Cliente-
Servidor
Máquina virtual

En informática una máquina virtual es un software que emula a una computadora y puede
ejecutar programas como si fuese una computadora real. Este software en un principio fue
definido como “un duplicado eficiente y aislado de una máquina física”. La acepción del
término actualmente incluye a máquinas virtuales que no tienen ninguna equivalencia
directa con ningún hardware real.

Una característica esencial de las máquinas virtuales es que los procesos que ejecutan están
limitados por los recursos y abstracciones proporcionados por ellas. Estos procesos no
pueden escaparse de esta “computadora virtual”.

Uno de los usos domésticos más extendidos de las máquinas virtuales es ejecutar sistemas
operativos para “probarlos”. De esta forma podemos ejecutar un sistema operativo que
queramos probar (GNU/Linux, por ejemplo) desde nuestro sistema operativo habitual (Mac
OS X por ejemplo) sin necesidad de instalarlo directamente en nuestra computadora y sin
miedo a que se desconfigure el sistema operativo primario.

Máquinas virtuales de sistema


Las máquinas virtuales de sistema, también llamadas máquinas virtuales de hardware,
permiten a la máquina física subyacente multiplicarse entre varias máquinas virtuales, cada
una ejecutando su propio sistema operativo. A la capa de software que permite la
virtualización se la llama monitor de máquina virtual o hypervisor. Un monitor de máquina
virtual puede ejecutarse o bien directamente sobre el hardware o bien sobre un sistema
operativo (“host operating system”).

Aplicaciones de las máquinas virtuales de sistema


 Varios sistemas operativos distintos pueden coexistir sobre la misma computadora, en sólido
aislamiento el uno del otro, por ejemplo para probar un sistema operativo nuevo sin necesidad de
instalarlo directamente.
 La máquina virtual puede proporcionar una arquitectura de instrucciones (ISA]) que sea algo
distinta de la verdadera máquina. Es decir, podemos simular hardware.
 Varias máquinas virtuales (cada una con su propio sistema operativo llamado sistema operativo
“invitado” o “guest”), pueden ser utilizadas para consolidar servidores. Esto permite que servicios
que normalmente se tengan que ejecutar en computadoras distintas para evitar interferencias, se
puedan ejecutar en la misma máquina de manera completamente aislada y compartiendo los
recursos de una única computadora. La consolidación de servidores a menudo contribuye a reducir
el coste total de las instalaciones necesarias para mantener los servicios, dado que permiten ahorrar
en hardware.
 La virtualización es una excelente opción hoy día, ya que las máquinas actuales (Laptops, desktops,
servidores) en la mayoría de los casos están siendo “sub-utilizados” (gran capacidad de disco duro,
memoria RAM, etc.), llegando a un uso de entre 30% a 60% de su capacidad. Al virtualizar, la
necesidad de nuevas máquinas en una ya existente permite un ahorro considerable de los costos
asociados (energía, mantenimiento, espacio, etc).
Modelo Cliente-Servidor

La arquitectura cliente-servidor consiste básicamente en un cliente que realiza peticiones


a otro programa (el servidor ) que le da respuesta. Aunque esta idea se puede aplicar a
programas que se ejecutan sobre una sola computadora es más ventajosa en un sistema
operativo multiusuario distribuido a través de una red de computadora.
La separación entre cliente y servidor es una separación de tipo lógico, donde el servidor no
se ejecuta necesariamente sobre una sola máquina ni es necesariamente un sólo programa.
Los tipos específicos de servidores incluyen los servidores web , los servidores de archivo,
los servidores del correo, etc.

La arquitectura cliente-servidor sustituye a la arquitectura monolítica en la que no hay


distribución, tanto a nivel físico como a nivel lógico.
La red cliente-servidor es aquella red de comunicaciones en la que todos los clientes están
conectados a un servidor, en el que se centralizan los diversos recursos y aplicaciones con
que se cuenta; y que los pone a disposición de los clientes cada vez que estos son
solicitados.

¿Qué es la virtualización?
15 abril, 2014 por J053M4 3ZQU3RR4

El hardware informático x86 actual está diseñado desde sus orígenes para ejecutar
al mismo tiempo un sólo sistema operativo, lo que supone la infrautilización de
gran parte de las máquinas.

La virtualidad supone una suplantación de la realidad mediante alguna


interfaz; entendida como una capa de abstracción que separa el
comportamiento del funcionamiento: Lo que se cree de lo que realmente es.
La virtualización es una capa lógica de Hw/Sw que esconde los elementos
reales para presentar unos recursos e información (sistemas virtuales) al usuario
sin necesidad de acceder directamente a los elementos discretos.
La capa de abstracción de hardware (Hardware Abstraction Layer ó HAL) es un
capa entre el hardware y el sistema operativo que funciona como
una interfaz entre el software y el hardware del sistema, proveyendo una
plataforma de hardware consistente y cerrada sobre la cual correr las aplicaciones.
Cuando se emplea una HAL, las aplicaciones no acceden directamente al hardware
sino que lo hacen a la capa abstracta provista por la HAL.

Está implementada como una biblioteca de enlace dinámico (dll) y es


responsable de proteger el resto del sistema de las especificaciones del hardware,
tales como controladores de interrupción e interfaces de entrada/salida.

Hipervisores
Un hipervisor o monitor de máquina virtual es un código implementado en el
ordenador para establecer una plataforma de control de virtualización.
Se crearon en los años 70, mediante una compleja programación que agrupaba
o consolidaba varias computadoras en un mainframe con el objetivo de reducir
costos. IBM fue uno de los creadores e impulsores de esta tecnología, IBM 7044, el
Sistema de Tiempo Compartido Compatible (CTSS –Compatible
Time SharingSystem). El sistema operativo se llamó Supervisor.
En la década de los 80 se abandonó la virtualización por hipervisor, por no
encontrarle una exacta funcionalidad y fundamentalmente, por la complejidad de
la arquitectura X86 de los ordenadores compatibles (PC).

A principios de los 90 se crearon los primeros hipervisor para PC, relanzando esta
técnica que hoy en día es fundamental en cualquier entorno de sistemas.
El hipervisor constituye una plataforma de virtualización, que se define como
un conjunto de software y hardware que simula la ejecución de equipos o sistemas
operativos distintos a los reales. Esto se consigue ocultando las características
físicas de la plataforma real y proporcionando otra plataforma abstracta y
simulada.

Los hipervisores se clasifican en 2 clases fundamentales:


HIPERVISOR nativo baremetal – unhosted
Llamada también virtualización por hardware, el software se ejecuta
directamente sobre el hardware del equipo, que controla el mismo y monitoriza los
sistemas operativos que se virtualizan.

Los sistemas virtualizados se ejecutan a un nivel superior al hipervisor.


HIPERVISOR completo hosted
Software que se ejecuta sobre un sistema operativo convencional (lo precisa para
ejecutarse sobre él), con el propósito de virtualizar sistemas. Se denomina
también virtualización por software o completa.

La virtualización se produce en una capa alejada del hardware. Por lo tanto, el


rendimiento es menor que el nativo.

Máquinas virtuales
La virtualización permite ejecutar una o varias máquinas virtuales en una misma
máquina física, donde cada una de las máquinas virtuales comparte los
recursos de ese único ordenador físico entre varios entornos. Las distintas
máquinas virtuales pueden ejecutar sistemas operativos diferentes y varias
aplicaciones en el mismo ordenador físico.
Por lo tanto, podemos definir una máquina virtual como un software que corre en
un entorno de ejecución real, pero embebido en otro entorno físico, que es el que
le proporciona los recursos que administra.

Virtualización de servidores
También llamada consolidación de servidores es una tecnología clave para
mejorar la utilización de los servidores físicos y por lo tanto de cualquier centro de
datos.
Mediante el uso de la tecnología de virtualización de servidores, las organizaciones
mejoran enormemente la utilización de sus servidores actuales pasando de utilizar
un 10% a un 70-80% de su capacidad total de cálculo, haciendo así un uso más
completo de los servidores que adquieren. Paralelamente, la administración se
facilita, se multiplican las posibilidades de medios y se economizan recursos.

El software de virtualización de servidores (Vmware Server, Sphere, ESXi, Xen,


HyperV, RedHatVirtualization por poner un ejemplo) es un software de máquina
virtual que se instala directamente en el servidor físico para consolidarlo y
particionarlo. Este software nos permite crear múltiples instancias de máquinas
virtuales en un mismo servidor físico, cada una de las cuales corre
independientemente un sistema operativo y aplicativo diferente.

Ventajas y desventajas de
la Virtualización

VENTAJAS
 Seguridad externa
 Aislamiento: El Sistema anfitrión no corre riesgo alguno
 Fácil migración
 Mayor aprovechamiento de recursos
 Migración en vivo
 Ahorro: energético, espacio físico, refrigeración, administración
DESVENTAJAS
 Muchos sistemas dependen de un solo equipo
 No existe un software libre de virtualización consolidado

Você também pode gostar