Multithread Programs

Anuncio
Prese nta d o e n el V Congres o Inter nacional Suda m e ricano d e Inge niería d e
Siste m a s e Infor m á tica. Areq ui pa - Peru. Oct ober 200 1
Aplicaciones Multi - Hebras
Ernesto Cuadros Vargas 1
Instituto de Ciências Matemáticas e de Computação ICMC
Universidade de São Paulo Campus de São Carlos
Av. do Trabalhador São- Carlense, 400 - Centro - Cx. Postal 668 CEP
13560- 970
São Carlos - SP – Brasil
e- mail: ecuadros@icmc.sc.usp.br
Resumen
Tradicionalmente nuestros programas se ejecutan a través de una
única hebra de ejecución desde el inicio hasta la finalización del
mismo. Es bastante conocido que a lo largo de la ejecución y
dependiendo del tipo de proceso existe una cantidad considerable de
tiempo ocioso del sistema que podria ser aprovechado para ejecutar
otras tareas pero, si el programa fue proyectado para una sola hebra,
esto no será tan facil de realizar. Una de las formas de permiten
aprovechar el tiempo ocioso de un proceso es la utilización de más de
una única hebra dentro del mismo proceso. Este paper presentará
diversas situaciones en las cuales es apropiado utilizar hebras y
presentará la forma en la cual esto sucede en la plataforma Win32 a
través de un ejemplo práctico.
1. INTRODUCCIÓN
Antes de entender que es una hebra debemos entender las diferencias
básicas entre Programa, Proceso y Hebra o Hilos. El primero de ellos, el
programa, es simplemente un archivo que tiene código ejecutable y
reside en un dispositivo de almacenamiento secundario (frecuentemente
en un disco duro). Una vez que el usuario solicita la ejecución de un
programa, éste es cargado por el Sistema Operativo (SO) y desde este
momento es considerado un proceso. En otras palabras, un proceso es
un programa que ya fue cargado y está en condiciones de ejecución. Una
hebra es, básicamente, la secuencia de ejecución de instrucciones de un
programa. Tradicionalmente, un proceso posee una única hebra pero
esto no es una regla. Existen diversas situaciones en las cuales es
adecuado tener más de una hebra en el mismo proceso [Andrews- 99]. En
1
Este trabajo tiene el apoyo financiero de la FAPESP- Brasil con el código de
proceso 99/ 118 35 - 7.
este último caso, todas las hebras que pertenecen al mismo proceso
pueden ser llamadas hermanas. Desde el punto del SO, la planificación
de procesos se realiza a nivel de hebras.
Cuando trabajamos con programas Multi - Hebras (MT 2 ) surgen nuevos
problemas que nunca serían considerados como tales en sus similares
con una única hebra (ST3 ). La programación de MT exige un mayor
cuidado , por ese motivo, cada SO ofrece diversos recursos (diferentes
para cada SO). Por ese motivo y para poder mostrar un ejemplo concreto,
el
resto
de
este
artículo
considerará
las
primitivas
existentes,
únicamente, en la plataforma Win32 . Para ver información adicional ver
[Beveridge- 96, Richter- 94]. El lenguaje utilizado para los ejemplos será
Visual C++ 6.0 por las facilidades para el trabajo con hebras [Hughes97,
Walmsley- 00].
Los SO disponibilizan
algunas
mecanismos
de
sincronización tales como: semáforos, mutex , entre otros.
También es frecuente pensar que los programas MT sólo son útiles
cuando disponemos de más de un procesador. Debe quedar claro que la
elección de trabajar o no con hilos depende de la naturaleza del
problema y no del tipo de computador en el cual será utilizado el
programa.
El resto de este artículo está organizado como sigue. En la sección 2 se
presentan los componentes comunes a un proceso y a una hebra. En la
sección 3 presentamos la forma de sincronizar secciones críticas. En la
sección 4 son presentados algunos problemas comunes relacionados al
manejo de hebras. En la sección 5 se pueden observar los dos tipos mas
comunes de hebras existentes. En la sección 6 se presentarán las
primitivas de comunicación utilizadas para la comunicación entre hebras.
En la sección 7 pueden ser observadas algunas sugerencias a ser
tomadas en consideración. En la sección 8 se presentan las conclusiones
y a continuación la bibliografía. Finalmente, en el apéndice se encuentra
el listado de una clase generada para administrar hebras utilizada como
ejemplo.
2. COMPOSICIÓN DE UNA HEBRA
Existen elementos comunes y particulares entre un proceso y una hebra.
Cada hebra posee:
•
2
3
una Pila (Stack),
Las siglas MT correspon d e n al término en inglés Multi- Thread.
Las siglas ST correspon de n al término en inglés Single- Thread.
•
una copia del estado de los registros de de la CPU,
•
un contador de programa (Program Counter ) y
•
una entrada en la lista de la ejecución del Planificador de Procesos
del Sistema Operativo (Scheduler ).
Por otro lado, todos las hebras pertenecientes a un mismo proceso
comparten los siguientes elementos:
•
archivos abiertos,
•
variables globales,
•
mecanismos de sincronización (semáforos, mutex , etc) y
•
la memoria asignada dinámicamente.
Todas las hebras existentes en un computador compitirán por ganar el
(los) procesador(es). Esta competencia se realiza utilizando filas del tipo
FIFO basadas en la prioridad de cada hebra. Aquellas hebras con mayor
prioridad tendrán mayor acceso al procesador. El scheduler determina
cual de los hilos debe tener el procesador, así como, el tiempo que
permanecerá activo 4 . Las hebras de baja prioridad tendrán que esperar
mientras las hebras de prioridades más altas completan sus tareas.
En máquinas con múltiples procesadores, el scheduler puede mover los
hilos individuales a procesadores diferentes buscando el “balance” de la
carga
de
los
procesadores.
Cada
hilo
en
un
proceso
opera
independientemente. A menos que, explícitamente, forcemos a que un
hilo dependa de otro, ellos se ejecutan individualmente e ignoran la
existencia de los demás. Sin embargo, cuando los hilos comparten
recursos comunes, deben coordinar su trabajo usando algún Método de
Comunicación entre Procesos (Inter process Communication - IPC) los
mismos que serán discutidos en ls siguiente sección.
3. SINCRONIZACIÓN ENTRE HEBRAS
El acceso a recursos comunes desde diferentes hilos es un problema
común cuando escribimos programas MT. Cuando dos o más hilos tratan
de accesar simultáneamente los mismos datos los resultados pueden ser
imprevisibles. Un ejemplo muy simple sería tratar de imprimir desde dos
hebras como se puede ver en la Figura 1.
/ / Hebra 1
4
/ / Hebra 2
La unidad de tiempo utilizada para que un proceso ‘posea’ la CPU es llamada
Quantu m .
(1) if( !PrinterBusy )
{
(2) ► PrinterBusy = true;
(3)
MyPrint(“UNSA.txt”);
}
(1)► If( !PrinterBusy )
{
(2)
PrinterBusy = true;
(3)
MyPrint(“Vitae.txt”);
}
Figura 1. Acceso al mismo recurso desde hebras diferentes sin
mecanismos de sincronización.
Supongamos, inicialmente, que la Hebra 1 estaba activa y la impresora
está desocupada (PrinterBusy = false). Eso significa que obtuvo el
acceso al if , pero no ejecutó la instrucción PrinterBusy = true;. Por otro
(1)
/ / Hebra 1
PrinterMutex.Lock();
(1)
/ / Hebra 2
PrinterMutex.Lock();
(2)
MyPrint(“UNSA.txt”);
(2)
MyPrint(“UNSA.txt”);
(3)
PrinterMutex.Unlock();
(3)
PrinterMutex.Unlock();
lado, en una aplicación MT no podemos decidir el instante preciso en el
cual
“perderemos
el procesador”.
Siendo
un
poco
pesimisma,
el
scheduler podría suspender la hebra 1 (antes de ejecutar la línea (2)) y
asignarle el mismo a la hebra 2. En ese instante esta última ejecutaría la
instrucción If( !PrinterBusy ) cuyo resultado es positivo (recordemos que
la hebra 1 no llegó a ejecutar la instruccion (2)). Esta situación simple
nos permite ver que no es posible controlar el acceso, inclusive a una
función, con esta técnica. El problema está en que las líneas (1) y (2) del
código deberían ser atómicas, lo cual significa que: “o se ejecutan ambas
o no se ejecuta ninguna”. El mismo código pero utilizando un mecanismo
de sincronización sería:
Figura 2. Acceso al mismo recurso desde hebras diferentes con
mecanismos de sincronización.
En este caso, el objeto utilizado para la sincronización es un mutex 56 . Si
dos o más hebras ejecutan la instrucción PrinterMutex.Lock(); el SO
garantiza que sólo uno de ellas consiga el recurso. Las demás hebras
que deseen tener el mismo acceso deberán “esperar” hasta que el objeto
sea desbloqueado. Vale la pena aclarar que “esperar” no significa
consumo de tiempo de procesador, tampoco es un ciclo que estará
dando vueltas hasta que la condición se cumpla. Esta operación se lleva
a cabo suspendiendo la hebra y poniéndola en estado de bloqueado
5
En este caso se supone que el mutex debe haber sido inicializado como
desbloquea do.
6
En Win32 existen los objetos de sincronización CriticalSection , Event , Mutex y
Semap hore .
(paso 3 de la Figura 3). Todas las hebras que estén bloqueadas por el
mismo
recurso
forman
una
lista.
Una
vez
que
el
recurso
sea
desbloqueado, el SO “despertará” la hebra que se encuentre a la cabeza
de la lista con lo cual estára en condiciones de competir por el
procesador nuevamente 7 como puede ser observado en el paso 4 de la
Figura 3.
2
Listos (Ready)
1
4
Corriendo (Running)
Bloqueado (Blocked)
3
Bloqueado (Blocked)
Bloqueado (Blocked)
Figura 3. Estados de un Proceso.
Probablemente la jerarquía de clases más conocida comercialmente sea
MFC
(Microsoft
Foundation
Clases)
[Msdn- 98].
Esta
jerarquía
disponibiliza cuatro clases para sincronización: CEvent, CCriticalSection,
CMutex y CSemaphore.
A grandes rasgos, CEvent, CCriticalSection y CMutex son mecanismos
que
aceptan
sólo
dos
estados
(bloqueado
y
desbloqueado)
y
CSemaphore tiene la capacidad de gerenciar múltiples estados . Por otro
lado, CCriticalSection sólo debe ser utilizado dentro del mismo proceso
mientras que, CEvent, CMutex y CSemaphore pueden ser usados para
sincronizar hebras de procesos diferentes.
4. PROBLEMAS GENERADOS POR UNA SINCRONIZACIÓN DEFICIENTE
Existen muchos problemas relacionados a la sincronización de procesos
y/o hebras. Este artículo se presentarán los deadlocks y el diseño de
clases seguras para ser utilizadas en un ambiente de múltiples hebras.
4.1. DEADLOCKS
Uno de los problemas mas conocidos es llamado deadlock y se presenta
cuando dos hebras o más hebras se bloquean mutuamente. Supongamos
que existen dos recursos R1 y R2 y dos hebras H1 (que tiene acceso a
R1) y H2 (que tiene acceso a R2). Supongamos también, que ambas
hebras necesitan tener acceso al recurso que les falta. Como el recurso,
al cual no tenemos acceso, está bloqueado, ambas hebras pasarán al
estado de bloqueado indefinidamente como puede ser observado en la
Figura 4.
7
Otras situaciones por las cuales un proceso pasa al estado de bloqueado es
cuando se ejecuta una operación de I/O y los datos no están disponibles
inmediata me n t e.
Hebra 1
R1
Hebra 2
R2
deadlock
Figura 4. Bloqueo mutuo entre procesos (deadlock).
4.2. CLASES SEGURAS PARA TRABAJAR EN MÚLTIPLES HEBRAS
No es lo mismo diseñar una clase para un ambiente ST que hacerlo para
un MT. En este caso, debemos considerar que, un cierto método podría
estar siendo accesado simultáneamente por más de una hebra. Para
visualizar con mayor claridad este problema supongamos que tenemos
un único puntero a un objeto que controla el acceso a una Base de Datos
(BD) (declarado del tipo CDatabase de MFC). Si este objeto fuese
utilizado desde múltiples hebras los resultados son imprevisibles. El
problema surge cuando dos métodos (del mismo objeto) están siendo
accesados por dos hebras simultáneamente, ambas podrían modificar
una misma variable al mismo tiempo afectando el resultado de la otra
hebra. Este tipo de problemas nunca aparecería en programas ST.
El código presentado en la Figura 5 nos permite observar la forma en la
cual se controla el acceso a una área común a través de un objeto del
tipo CMutex.
HANDLE hIOMutex= ::CreateMutex (NULL, FALSE, NULL);
/ / Pedir acceso al recurso
::WaitForSingleObject ( hIOMutex, INFINITE );
/ / Realizar nuestra operación crítica
::fseek ( fp, desired_position, 0L );
::fwrite ( data, sizeof ( data ), 1, fp );
/ / Liberar el acceso al recurso
::ReleaseMutex (hIOMutex);
Figura 5. Acceso al disco sincronizado.
5. TIPOS DE HEBRAS
Existen varios tipos de hilos o hebras, pero las mas conocidas son las
Worker Threads (hebras trabajadoras) y las User- Interface Threads
(hebras que presentan algún mecanisno para que el usuario pueda
interactuar con las mismas). Ambos casos serán explicados con mayor
grado de detalle a continuación.
5.1. HEBRAS TRABAJADORAS
Una hebra trabajadora es comúnmente utilizada para realizar tareas en
segundo plano, osea, tareas que el usuario no necesita saber de forma
visible (por ejemplo,
viendo una ventana activa) que está siendo
ejecutada. Tareas tales, como imprimir en segundo plano o recalcular
algunos valores son buenos ejemplos de este tipo de hebras. Para
implementar este tipo de hebras debemos considerar los siguientes
pasos:
•
implementar la función que controlará la hebra y
•
crear la hebra.
Crear una hebra es una tarea relativamente simple. Sólo se necesitan
esos dos pasos para iniciar su ejecución. En la plataforma Win32 es
posible crear hebras de varias formas, cada una de ellas exige que la
funcion tenga un prototipo específico. En este caso será mostrado un
ejemplo considerando la funcion CreateThread . El prototipo de la
función para crear la hebra en este caso sería:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
El parámetro de la función nos permite enviar, de forma opcional,
información inicial para la hebra. En este parámetro podríamos enviar,
“disfrazado” como un void *, punteros a objetos, variables, números
enteros, etc. La interpretación de este parámetro depende del usuario.
Por ejemplo, si sabemos que la hebra debe recibir un CStudent *, el
parámetro recibido debería ser interpretado como se observa en la
Figura 6.
DWORD WINAPI MyThreadMain( LPVOID lpParameter)
{
CStudent *pMyObj = (CStudent *)lpParameter;
pMyObj - >MyMethod1();
pMyObj - >MyMethod2();
.
.
.
return 0L;
}
Figura 6. Interpretación del parámetro recibido por una Worker
Thread .
Para una hebra, esta función corresponde a su función main. El prototipo
de la función CreateThread es:
HANDLE CreateThread (
/ / puntero a los atributos de securidad
LPSECURITY_ATTRIBUTES lpThreadAttributes,
/ / Tamaño inicial del Stack para esta hebra
DWORD dwStackSize,
/ / puntero a la función de la hebra
LPTHREAD_START_ROUTINE lpStartAddress,
/ / argumento para la nueva hebra
LPVOID lpParameter,
/ / atributos de creación
DWORD dwCreationFlags,
/ / puntero para recibir el ID de la hebra
LPDWORD lpThreadId );
Figura 7. Prototipo de la función CreateThread .
Considerando la función MyThreadMain , una forma adecuada de crear
una hebra utilizando CreateThread sería:
CStudent MyStudent;
DWORD ThreadID;
HANDLE hThread = ::CreateThread (NULL, 4096, &MyThreadMain,
(LPVOID)&MyStudent, 0,
&ThreadID);
Figura 8. Creación de una hebra a través de la función CreateThread .
suponiendo que deseamos esperar hasta que la hebra termine su
ejecución, el código adecuado sería:
::WaitForSingleObject (hThread, INFINITE);
el segundo parámetro (en este caso INFINITE) determina el número de
milisegundos que se debe esperar. La constante INFINITE instruye al SO a
esperar hasta que la hebra concluya sin importar el tiempo que eso
signifique.
En relación a la prioridad de una hebra podemos decir que: existe una
prioridad por defecto, pero nosotros podemos modificarla de la siguiente
forma:
::SetThreadPriority (hThread, THREAD_PRIORITY_ABOVE_NORMAL);
las posibles prioridades son:
•
THREAD_PRIORITY_ABOVE_NORMAL,
•
THREAD_PRIORITY_BELOW_NORMAL,
•
THREAD_PRIORITY_HIGHEST,
•
THREAD_PRIORITY_IDLE,
•
THREAD_PRIORITY_LOWEST,
•
THREAD_PRIORITY_NORMAL,
•
THREAD_PRIORITY_TIME_CRITICAL.
si deseamos cambiar la prioridad por defecto de las hebras que serán
creadas
en
un
proceso,
la
función
que
debe
ser
utilizada
es
SetPriorityClass. Suponiendo que el objetivo sea cambiar la prioridad
dentro de nuestro proceso, el código adecuado sería el siguiente:
::SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
las prioridades posibles en este caso son:
•
HIGH_PRIORITY_CLASS,
•
IDLE_PRIORITY_CLASS,
•
NORMAL_PRIORITY_CLASS,
•
REALTIME_PRIORITY_CLASS.
Si deseamos suspender temporalmente una hebra activa podemos
utilizar la función SuspendThread de la siguiente forma:
::SuspendThread (hThread);
así mismo, si deseamos continuar la ejecución de una hebra suspendida
lo podemos hacer de la siguiente forma:
::ResumeThread (hThread);
Todas estas primitivas son aplicables para todos los tipos de hebras,
inclusive las que interactúan con el usuario a través de ventanas.
5.2. HEBRAS QUE INTERACTÚAN CON EL USUARIO
Este tipo de hebras son utilizadas cuando el usuario necesita interactuar
a través de una ventana, enviar algún mensage o responder a ciertos
eventos. Dicho en otras palabras la hebra tendrá una parte visible
(generalmente una ventana). En este caso podemos utilizar la clase
CWinThread de la jerarquía MFC. Esta clase se encarga de administrar
todos los aspectos internos de la creación de la hebra. Esa clase también
disponibiliza
la función
virtual
InitInstance que el usuario
puede
redefinir con el objetivo de tomar el control de la misma, crear su propia
ventana, etc.
A continuación detallaremos los pasos para crear una hebra con interface
de usuario. En primer lugar, nuestra clase debe ser heredada de la clase
CWinThread . La secuencia de pasos para crear el código para la nueva
clase son:
Paso 1.- Ir al menú Insert | New Class.
Figura 9. Creación de una nueva clase para administrar hebras (Paso
1).
Paso 2.- Seleccionar CWinThread como la clase base.
Figura 10 . Creación de una nueva clase para administrar hebras (Paso
2).
El código generado está disponible en el Apéndice A.
Suponiendo que deseamos crear una nueva hebra de la clase recién
creada en el método CMainFrame::OnNuevaThread, el código adecuado
sería el observado en la Figura 11.
void CMainFrame::OnNuevaThread()
{
/ / TODO: Add your command handler code here
CRuntimeClass *pRuntimeClass = RUNTIME_CLASS(CMyThread);
CMyThread *pMyThread =
(CMyThread *)pRuntimeClass- > CreateObject ();
pMyThread- > CreateThread ();
}
Figura 11 . Forma de activar una hebra.
Internamente, la clase creada llamará a la función virtual InitInstance la
cual estará inicialmente vacía como puede ser visto en la Figura 12.
BOOL CMyThread::InitInstance()
{
return TRUE;
}
Figura 12 . Función virtual InitInstance inicialmente vacía.
Podríamos considerar esa función como nuestro main, por lo tanto, el
código para crear una nueva ventana debe ser incluído en ella. Sólo por
facilidad vamos a suponer que deseamos crear una ventana similar a la
principal. En ese caso el código sería el presentado en la Figura 13.
BOOL CMyThread:: InitInstance ()
{
CSingleDocTemplate * pDocTemplate = new
CSingleDocTemplate (
IDR_MAINFRAME,
RUNTIME_CLASS(CTeste2Doc), / / clase del documento
RUNTIME_CLASS(CMainFrame), / / clase del MainFrame
RUNTIME_CLASS(CTeste2View));/ / clase del área de cliente
/ / Crear un nuevo documento para esta ventana
CDocument *pDoc = pDocTemplate- > CreateNewDocument ();
/ / Crear la ventana propiamente dicha
m_pMainWnd = pDocTemplate- > CreateNewFrame (pDoc, NULL);
/ / Hacerla visible
m_pMainWnd - > ShowWindow (SW_SHOW);
/ / Enviarle un mensage de actualizacion
m_pMainWnd - > UpdateWindow ();
return TRUE;
}
Figura 13 . Código necesario para crear una ventana para la nueva
hebra.
6. COMUNICACIÓN ENTRE HEBRAS
Del mismo modo que los objetos, las hebras se comunican a través de
mensajes. En la Programación Orientada a Objetos (POO), enviar un
mensaje significa llamar a un método de un objeto. Ya en una plataforma
orientada a ventanas como es el caso de la plataforma Windows, enviar
un mensaje a una ventana significa depositar el mensaje en una cola de
mensajes administradas por el SO. Existen dos primitivas básicas para
enviar un mensaje a una ventana: SendMessage y PostMessage.
La diferencia radica en que PostMessage deposita el mensaje y retorna
el control al usuario. Si la ventana fue creada dentro de otra hebra, el
mensaje podría ser despachado en paralelo. Así mismo, si la ventana que
recibirá el mensaje pertenece a la misma hebra, debemos devolver el
control al Kernel del SO para que este pueda despacharlo. Cuando eso
suceda, el SO verá que hay un mensaje a la espera de ser procesado y lo
despachará.
La función SendMessage, además de depositar el mensaje en la cola de
mensajes de la ventana, espera a que éste sea procesado antes de
devolver el control al usuario.
Haciendo una analogía, cuando vamos al correo y sólo dejamos una carta
estamos haciendo lo mismo que haría PostMessage pero, si nos
quedamos a esperar hasta que la carta llegue a su destino estaríamos
comportándonos como lo hace SendMessage. Considerando como base
estas dos primitivas, es lógico pensar que los mensajes de una ventana
siempre deben ser procesados cuando la hebra que la creó esté activa.
Esto significa que no es recomendable utilizar SendMessage con una
ventana que pertenece a otra hebra 8 .
Para enviar mensajes entre hebras existe la función PostThreadMessage
que tiene un comportamiento similar a PostMessage pero sirve para
hebras. Los prototipos de estas funciones utilizando MFC son:
8
Es necesario recordar
inmediata me n t e.
que
SendMessage
intentaría
procesar
el mensaje
LRESULT SendMessage(
UINT message,
/ / Mensaje
WPARAM wParam = 0, / / Primer parámetro opcional
LPARAM lParam = 0); / / Primer parámetro opcional
BOOL
PostMessage (
UINT message,
/ / Mensaje
WPARAM wParam = 0, / / Primer parámetro opcional
LPARAM lParam=0
/ / Segundo parámetro opcional
);
BOOL PostThreadMessage(
UINT message ,
/ / Mensaje
WPARAM wParam,
/ / Primer parámetro opcional
LPARAM lParam
/ / Segundo parámetro opcional
);
Figura 14 . Prototipos de las funciones CWnd::SendMessage,
CWnd::PostMessage y CWinThread::PostThreadMessage 9 .
Las funciones nativas de la plataforma Win32 son similares, la única
diferencia es que reciben un parámetro adicional que es el HWND 10 de la
ventana para SendMessage y PostMessage y el ThreadID de la hebra en
el caso de PostThreadMessage 11 . Los prototipos pueden ser observados
a continuación:
LRESULT SendMessage(HWND hWnd, UINT Msg,
WPARAM wPar, LPARAM lParam);
BOOL
PostMessage(HWND hWnd, UINT Msg,
WPARAM wPar, LPARAM lParam);
BOOL PostThreadMessage(DWORD idThr, UINT Msg,
WPARAM wParam, LPARAM lParam);
Figura 15 . Prototipos de las funciones API12 de Win32 SendMessage,
PostMessage y PostThreadMessage
Suponiendo que deseamos enviar un mensaje a una ventana en MFC,
debemos seguir los siguientes pasos:
9
SendMessage y PostMessage pertenecen a la clase CWnd y PostThreadMessage
a la clase CWinThread .
10
El tipo HWND es un tipo predefinido que representa una estructura que
contiene la información relacionada a una ventana.
11 El ThreadID es el valor retorna do en el último parámet ro cuando llamam os la
función CreateThread .
12 API viene de las siglas en inglés de Application Progra m m i n g Interface .
Paso 1.- Crear la función que recibirá el mensaje. El prototipo debe ser
el siguiente:
afx_msg LRESULT
MyFuncion (WPARAM wParam, LPARAM lParam);
Figura 16 . Creación de una función para recibir un mensaje (Paso 1- a).
Figura 17 . Creación de una función para recibir un mensaje (Paso 1b).
Paso 2.- Crear el cuerpo de la función:
LRESULT CMyThread::MyFuncion( WPARAM wParam, LPARAM
lParam)
{
/ / Aqui debemos agregar nuestro código
return 0L;
}
Figura 18 . Creación de una función para recibir un mensaje (Paso 2).
Paso 3.- Mapear el mensaje a la función. Hasta este momento, nuestro
código está preparado para recibir el mensaje pero todavía no hemos
vinculado el mensaje a la función. Este vínculo es conocido como mapear
el mensaje a una función y se realiza de la siguiente forma:
BEGIN_MESSAGE_MAP(CMyThread , CWinThread)
/ /{{AFX_MSG_MAP(CMyThread)
ON_THREAD_MESSAGE(WM_MYMESSAGE, MyFuncion)
/ /}}AFX_MSG_MAP
END_MESSAGE_MAP()
Figura 19 . Relacionando un mensaje a una función en una clase
descendiente de CWinThread (Paso 3).
Si fuese el caso de una clase descendiente de una ventana, por ejemplo
de un CEdit, la forma de mapear el mensaje es:
BEGIN_MESSAGE_MAP(CMyEdit, CEdit)
/ /{{AFX_MSG_MAP(CMyThread)
ON_MESSAGE(WM_MYMESSAGE, MyFuncion)
/ /}}AFX_MSG_MAP
END_MESSAGE_MAP()
Figura 20 . Relacionando un mensaje a una función en una clase
descendiente de CWnd (Paso 3).
La declaración de la constante WM_MYMESSAGE debe ser de la siguiente
forma:
#define WM_MYMESSAGE (WM_USER+1)
Nuevos mensajes deben ser creados con (WM_USER+2), (WM_USER+3),
etc.
7. SUGERENCIAS
Como ya fue mencionado antes, programar múltiples hebras requiere de
mucho
cuidado,
por
eso
debemos
considerar
las
sugerencias
presentadas a continuación.
7.1. LA NATURALEZA DEL PROBLEMA
La utilización de hebras depende de la naturaleza del problema más que
del tipo de computador donde vaya a ser usado el programa. Por
ejemplo, el algoritmo de ordenamiento QuickSort primero escoge un
pivote e ubica los valores menores a la izquierda y los mayores a la
derecha del mismo. Una vez realizada esa operación, QuickSort se llama
recursivamente con ambos bloques. En este caso ambas llamadas son
totalmente independientes por lo cual podrían ser programadas usando
dos hebras independientes.
7.2. LA ATOMICIDAD DEL CÓDIGO
Debemos tener en cuenta siempre, que el Scheduler podría suspender la
ejecución de una hebra en cualquier línea (ver Figura 1). Si deseamos
ejecutar más de una instrucción de forma atómica, debemos utilizar
mecanismos de sincronización.
7.3. ESTADOS DE ESPERA ÓPTIMOS
Si una hebra desea accesar a un recurso no es adecuado utilizar ciclos
hasta que una condición se cumpla. Hay que recordar que esa técnica
consume tiempo del procesador. Un estado de espera óptimo no
consume procesador y corresponde al estado de bloqueado (ver el
estado de bloqueado de la Figura 3). Una hebra bloqueda por un
mecanismo de sincronización será activada automáticamente por el SO
cuando el recurso sea liberado.
7.4. LA CANTIDAD DE HEBRAS APROPIADAS
El poco uso de hebras o el abuso de las mismas puede resultar
perjudicial,
disponemos
lo
adecuado
de un
único
es
buscar
procesador
el
balance.
Por
ejemplo,
no sería adecuado
si
ejecutar
QuickSort, con 1000 hebras. El motivo es muy simple, además del mayor
tiempo que el SO necesita para crear, administrar y finalizar una hebra
son necesarios recursos como memoria, etc. También hay que resaltar
que suspender una hebra y otorgarle el control a otra es un proceso que
consume tiempo. Mientras más hebras creemos, mayor será el tiempo
requerido por el SO para administrarlas, disminuyendo así, el tempo útil
para las aplicaciones.
Otro factor importante es considerar la proporción de tiempo ocioso
generado por las operaciones de Entrada/Salida (I/O) de cada hebra. Si
tuvieramos 80% de I/O significa que el procesador sólo tiene 20% de uso
efectivo. Ese número nos indica que podríamos tener, aproximadamente,
cinco hebras del mismo tipo y aprovechar mejor la capacidad ociosa del
procesador. Cuando tenemos hebras que realizan operaciones de I/O, su
número debería ser mayor que el número de procesadores. Si las hebras
sólo realizan cálculos sin I/O, es probable que no veamos mucha
diferencia si utilizamos más hebras que el número de procesadores
disponibles.
8. CONCLUSIONES
Entre las principales conclusiones relacionadas al trabajo
con MT
podemos mencionar las siguientes:
•
la utilización o no de hebras depende de la naturaleza del
problema y no del tipo de máquina en la cual va a ser ejecutado el
programa,
•
los programas MT pueden ejecutarse sin problemas en un único
procesador pero, la gran diferencia es que, están preparados para
aprovechar la existencia de más procesadores y el tiempo ocioso
de las demás hebras del sistema,
•
la forma de garantizar la ejecución de un grupo de instrucciónes
en forma atómica es a través de mecanismos de sincronización,
•
la forma de esperar por un recurso no debe envolver la utilización
de ciclos ya que esa técnica consume procesador,
•
el poco uso de hebras o su uso excesivo genera problemas porque
el SO necesita tiempo y recursos para administrarlas,
•
el número de hebras adecuadas está vinculado a la cantidad de
operaciones de I/O que los procesos presenten,
BIBLIOGRAFÍA
[Andrews- 99]
ANDREWS G. R. Foundations of Multithreaded,
[Beveridge- 96]
Parallel, and Distributed . Addison- Wesley . 1999
BEVERIDGE J.; WIENER R.; BEVERIDGE, J. E.
Multithreading Applications in Win32 : The Complete
[Hughes- 97]
Guide to Threads . Addison- Wesley. 1996.
HUGHES C; HUGHES T. Object- Oriented
[Msdn- 98]
Multithreading Using C++ . John Wiley & Sons. 1997.
MICROSOFT CORP.©. Microsoft Developer Network
Library Readme. URL:http: / / ms dn.microsoft.com.
[Richter - 94]
1998.
RICHTER J. Advanced Windows NT. Microsoft Press,
[Walmsley- 00]
1994.
WALMSLEY M. Multi - Threaded Programming in C++ .
Springer Verlag. 2000.
Apéndice A
MYTHREAD.H
class CMyThread : public CWinThread
{
DECLARE_DYNCREATE(CMyThread)
protected :
/ / protected constructor used by dynamic creation
CMyThread();
public : / / Attributes
/ / Operations
public :
/ / Overrides
/ / ClassWizard generated virtual function overrides
/ /{{AFX_VIRTUAL(CMyThread)
public :
virtual BOOL InitInstance();
virtual int ExitInstance();
/ /}}AFX_VIRTUAL
/ / Implemen ta tion
protected :
virtual ~CMyThread();
/ / Generated message map functions
/ /{{AFX_MSG(CMyThread)
/ / NOTE - the ClassWizard will add and remove member
functions here.
/ /}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
MYTHREAD.CPP
/ / MyThread.cpp : implementation file
//
#include "stdafx.h"
#include "teste2.h"
#include "MyThread.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
//
/ / CMyThread
IMPLEMENT_DYNCREATE(CMyThread, CWinThread )
CMyThread::CMyThread()
{
}
CMyThread::~CMyThread()
{
}
BOOL CMyThread::InitInstance ()
{
/ / TODO: perform and per- thread initialization here
return TRUE;
}
int CMyThread::ExitInstance ()
{
/ / TODO: perform any per- thread cleanup here
return CWinThread ::ExitInstance ();
}
BEGIN_MESSAGE_MAP(CMyThread, CWinThread )
/ / {{AFX_MSG_MAP(CMyThread)
/ / NOTE - the ClassWizard will add and remove mapping macros
here.
/ / }}AFX_MSG_MAP
END_MESSAGE_MAP()
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
//
Descargar