Esta es una copia cruzada del artículo original en mi blog:
http://juank.black-byte.com/sistemas_operativos-manejador-procesos-threads-fibers/
----
Hola, esta es la tercera parte de la serie de Sistemas
operativos, la cual confieso he abordado ahora con un objetivo
diferente al de enseñar solo sistemas operativos. Esta serie de
artículos esta direccionada a tener claros los fundamentos para luego
iniciar a trabajar con LA TPL (Task Parallel Library) incluida ahora en
el Framework 4.0.
Estos son los dos artículos anteriores:
Parte 1 : Manejador de Procesos – Fundamentos
Parte 2 : Manejador de Procesos – Procesos
En la primera parte identificamos la manera en que
trabaja una CPU y diferenciamos los sistemas mono tarea y los sistemas
multitarea donde pudimos ver como hace el sistema para ejecutar varios
procesos la vez, así mismo revisamos el tema de lo que es el estado de
ejecución de un proceso en un entorno multitarea.
En la segunda parte 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.
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 que 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. 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 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 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án 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 hayan
CPUS disponibles. Esto es muy engorroso de hacer pero es allí donde la
TPL es de gran ayuda.
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 corta 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
administrador 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 mas recomendable trabajar con Kernel
Threads que con Fibers, dada su mayor complejidad los fibers pueden
traer mas 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!
Estos son los dos artículos anteriores:
Parte 1 : Manejador de Procesos – Fundamentos
Parte 2 : Manejador de Procesos – Procesos
.
Esta es una copia cruzada del artículo original en mi blog:
http://juank.black-byte.com/c-conseguir-handler-ventana-proceso/
----
Cuando se esta jugando con la API de Windows,
especialmente con el tema de las ventanas esta función puede resultar
de muchísima utilidad. Sin embargo no existe, así que hay que
implementarla.
En resumen explicare los pasos necesarios para crear
una función GetProcessWindowHandler, la cual devuelve un handler a la
ventana principal de un proceso.
Necesitaremos recuperar el handler de la ventana
(cuando la encontremos),así que creare una clase que utilizare como
LPARAM a algunas funciones de la API, capaz de contener tanto el id del
proceso como el handler de la ventana.
/// <summary>Almacena el ID de proceso y el handler de una ventana</summary>
private class AuxInfo
{
public int processID;
public IntPtr handler;
}
Para lograrlo debemos recurrir a la función
EnumWindows, la utilizaremos para recorrer las ventanas existentes en
búsqueda de una ventana cuyo id de proceso coincida con el proceso que
acabamos de iniciar.
Como EnumWindows requiere como parámetro un delegado
que se ejecutara para las ventanas enumeradas, entonces la función de
búsqueda debe tener el signature de EnumWindowsProc, declarado en la
API de Windows y que acá lo declararo como un delegado.
/// <summary>
/// Delegado para hacer de callback
/// </summary>
/// <param name="hwnd" />handler de la ventana
/// <param name="lParam" />paramétro con la informacion necesaria para el proceso
/// <returns>Valor de retorno del proceso</returns>
private delegate bool EnumWindowsProc(IntPtr hwnd, AuxInfo lParam);
Y acá la definición de EnumWindows
/// <summary>
/// Recorre las ventanas y ejecuta un proceso para cada una de ellas
/// </summary>
/// <param name="lpEnumFunc" />Delegado con el proceso a utilizar para cada ventana
/// <param name="lParam" />paramétro con la informacion necesaria para el proceso
/// <returns>Retorna true si se recorren todas las ventanas, de lo contrario false o segun determine el usuario a trabes del callback</returns>
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, AuxInfo lParam);
Para poder determinar el id de proceso de cada una de las ventanas enumeradas haré uso de GetWindowThreadProcessId:
/// <summary>
/// Devuelve el ID del proceso al que pertenece el hilo de la ventana
/// </summary>
/// <param name="hwnd" />handler de la ventana
/// <param name="lpdwProcessId" />ID del proceso (parámetro de salida)
/// <returns>ID del Thread que creó la ventana</returns>
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out int lpdwProcessId);
Ya con esta información mi función delegada para
encontrar el handler de ventana ( la que se ejecutara por cada ventana
hallada por EnumWindows )es esta:
/// <summary>
/// Obtiene el handler de la ventana asociada a un proceso
/// Este procedimiento es solo de utileria para usarse con EnumWindows
/// y no deberia ser invocado directamente
/// </summary>
/// <param name="hwnd" />handler de la ventana actual
/// <param name="info" />informacion auxiliar para el proceso
/// <returns>false si encuentra la ventana, true sino</returns>
private static bool _GetProcessWindowHandler(IntPtr hwnd, AuxInfo info)
{
int processID;
GetWindowThreadProcessId(hwnd, out processID);
if (processID == info.processID)
{
info.handler = hwnd;
return false;
}
else
{
info.handler = IntPtr.Zero;
return true;
}
}
Estando ya definida mi función de callback entonces
llamaré a EnumWindows y crearé con ella una función
GetProcessWindowHandler, la cual como su nombre lo indica será la que
usaré para devolver el handler de la ventana del proceso. Sin embargo
el tema no es tan fácil como pareciera a simple vista.
Si utilizo la función para traer un handler a la
ventana de un proceso ya abierto no tengo ningún problema, pero si el
proceso recién lo estoy lanzando desde mi aplicación, por ejemplo con
Process.Start(), se debe esperar a que el sistema operativo cree y
muestre por primera vez la ventana, de lo contrario no habrá manera de
hallarla con EnumWindows, así que debo llamar a EnumWindows hasta que
se cumplan estas dos condiciones:
-
Encontró una ventana asociada al proceso
-
Dicha ventana ya ha sido mostrada por el sistema operativo
Para la primera condición, y de acuerdo a como creamos
nuestra función de callback (_GetProcessWindowHandler), basta con
preguntar si el handler es válido y para la segunda se debe determinar
si la ventana de dicho proceso ya ha sido mostrada lo cual lo hacemos
con IsWindowVisible:
/// <summary>
/// Indica si una ventana es o no visible
/// </summary>
/// <param name="hWnd" />handler de la ventana
/// <returns>Indicador de si la v entana es o no visible</returns>
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
Así que la función internamente debe tener un proceso iterativo para poder hallar el handler
/// <summary>
/// Devuelve el handler de la ventana asociada al proceso
/// </summary>
/// <param name="pid" />Id del proceso
/// <returns>handler de la ventana</returns>
public static IntPtr GetProcessWindowHandler(int pid)
{
//Delegado con el proceso auxiliar de búsqueda
EnumWindowsProc getHandlerVentana = new EnumWindowsProc(_GetProcessWindowHandler);
//Informacion auxiliar
AuxInfo informacion = new AuxInfo();
informacion.processID = pid;
/*Repetir bucle hasta que este presente la ventana del proceso
*(puede que la enumeracion se realice y windows aún no haya creado
*la primera ventana del proceso o bien no la haya hecho visible,
*por lo cual se debe repetir el bucle hasta encontrala)*/
do
{
/*Enumerar las ventanas buscando la que coincida con
*el id de proceso contenido en informacion */
EnumWindows(getHandlerVentana, informacion);
} while (informacion.handler == IntPtr.Zero || !IsWindowVisible(informacion.handler));
return informacion.handler;
}
Bien, he encapsulado la funcionalidad en la clase
Win32APITools y el método GetProcessWindowHandler es el único método
expuesto, asi que la implementación completa queda así:
using System;
using System.Runtime.InteropServices;
class Win32APITools
{
/// <summary>Almacena el ID de proceso y el handler de una ventana</summary>
private class AuxInfo
{
public int processID;
public IntPtr handler;
}
/// <summary>
/// Delegado para hacer de callback
/// </summary>
/// <param name="hwnd" />handler de la ventana
/// <param name="lParam" />paramétro con la informacion necesaria para el proceso
/// <returns>Valor de retorno del proceso</returns>
private delegate bool EnumWindowsProc(IntPtr hwnd, AuxInfo lParam);
/// <summary>
/// Recorre las ventanas y ejecuta un proceso para cada una de ellas
/// </summary>
/// <param name="lpEnumFunc" />Delegado con el proceso a utilizar para cada ventana
/// <param name="lParam" />paramétro con la informacion necesaria para el proceso
/// <returns>Retorna true si se recorren todas las ventanas, de lo contrario false o segun determine el usuario a trabes del callback</returns>
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, AuxInfo lParam);
/// <summary>
/// Devuelve el ID del proceso al que pertenece el hilo de la ventana
/// </summary>
/// <param name="hwnd" />handler de la ventana
/// <param name="lpdwProcessId" />ID del proceso (parámetro de salida)
/// <returns>ID del Thread que creó la ventana</returns>
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out int lpdwProcessId);
/// <summary>
/// Indica si una ventana es o no visible
/// </summary>
/// <param name="hWnd" />handler de la ventana
/// <returns>Indicador de si la v entana es o no visible</returns>
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
/// <summary>
/// Obtiene el handler de la ventana asociada a un proceso
/// Este procedimiento es solo de utileria para usarse con EnumWindows
/// y no deberia ser invocado directamente
/// </summary>
/// <param name="hwnd" />handler de la ventana actual
/// <param name="info" />informacion auxiliar para el proceso
/// <returns>false si encuentra la ventana, true sino</returns>
private static bool _GetProcessWindowHandler(IntPtr hwnd, AuxInfo info)
{
int processID;
GetWindowThreadProcessId(hwnd, out processID);
if (processID == info.processID)
{
info.handler = hwnd;
return false;
}
else
{
info.handler = IntPtr.Zero;
return true;
}
}
/// <summary>
/// Devuelve el handler de la ventana asociada al proceso
/// </summary>
/// <param name="pid" />Id del proceso
/// <returns>handler de la ventana</returns>
public static IntPtr GetProcessWindowHandler(int pid)
{
//Delegado con el proceso auxiliar de búsqueda
EnumWindowsProc getHandlerVentana = new EnumWindowsProc(_GetProcessWindowHandler);
//Informacion auxiliar
AuxInfo informacion = new AuxInfo();
informacion.processID = pid;
/*Repetir bucle hasta que este presente la ventana del proceso
*(puede que la enumeracion se realice y windows aún no haya creado
*la primera ventana del proceso o bien no la haya hecho visible,
*por lo cual se debe repetir el bucle hasta encontrala)*/
do
{
/*Enumerar las ventanas buscando la que coincida con
*el id de proceso contenido en informacion */
EnumWindows(getHandlerVentana, informacion);
} while (informacion.handler == IntPtr.Zero || !IsWindowVisible(informacion.handler));
return informacion.handler;
}
}
Y este es un ejemplo de uso:
using System;
using System.Diagnostics;
namespace GetProcessWindowHandler
{
class Program
{
static void Main(string[] args)
{
Process proc = new Process();
ProcessStartInfo psi = new ProcessStartInfo("calc.exe");
proc.StartInfo = psi;
proc.Start();
IntPtr handler = Win32APITools.GetProcessWindowHandler(proc.Id);
Console.WriteLine("El Handler obtenido para la ventana de este proceso es: {0}", handler);
Console.ReadLine();
}
}
}
Hasta Pronto.