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: [email protected] 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() // / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / //