Site hosted by Angelfire.com: Build your free website today!

Tutorial 15: Programación Multihilo

En este tutorial aprenderemos como crear un programa multihilos [multithreading program]. también estudiaremos los métodos de comunicación entre los hilos.

Bajar el ejemplo aquí.

Teoría:

En el tutorial previo, aprendiste que un proceso consta de al menos un hilo [thread]: el hilo primario. Un hilo es una cadena de ejecución. también puedes crear hilos adicionales en tu programa. Puedes concebir la programación multihilos [multithreading programming] como una programación multitareas [multitasking programming] dentro de un mismo programa. En términos de implementación, un hilo es una función que corre concurrentemente con el hilo principal. Puedes correr varias instancias de la misma función o puedes correr varias funciones simultáneamente dependiendo de tus requerimientos. La programación multihilos es específica de Win32, no existe una contraparte en Win16.

Los hilos corren en el mismo proceso, así que ellos pueden acceder a cualquiera de sus recursos tal como variables globales, manejadores etc. Sin embargo, cada hilo tiene su pila [stack] propia, así que las variables locales en cada hilo son privadas. Cada hilo también es propietario de su grupo de registros privados, así que cuando Windows conmuta a otros hilos, el hilo puede "recordar" su último estado y puede "resumir" la tarea cuando gana el control de nuevo. Esto es manejado internamente por Windows.

Podemos dividir los hilos en dos categorías:

  1. Hilo de interface de usuario: Este tipo de hilo crea su propia ventana, y así recibe mensajes de ventana. Puede responder al usuario a través de su propia ventana. Este tipo de hilo está sujeto a la regla del Mutex de Win16 que permite sólo un hilo de interface de usuario en el núcleo de usuario y gdi de 16-bit. Mientras el hilo de interface de usuario esté ejecutando código de núcleo de usuario y gdi de 16-bit, otros hilos UI no podrán usar los servicios del núcleo de usuario y gdi. Nota que este Mutex de Win16 es específico a Windows 95 desde su interior, pues las funciones de la API de Windows 95 se remontan [thunk down] hasta el código de 16-bit. Windows NT no tiene Mutex de Win16 así que los hilos de interface de usuario bajo NT trabajan con más fluidez que bajo Windows 95.
  2. Hilo obrero [Worker thread]: Este tipo de hilo no crea ninguna ventana así que no puede recibir ningún mensaje de ventana. Existe básicamente para hacer la tarea asignada en el trasfondo hence el nombre del hilo obrero.

Recomiendo la siguiente estrategia cuando se use la capacidad multihilo de Win32: Dejar que el hilo primario haga de interface de usuario y los otros hilos hagan el trabajo duro en el trasfondo. De esta manera, el hilo primario es como un Gobernador, los otros hilos son como el equipo del gobernador [Governor's staff]. El Gobernador delega tareas a su equipo mientras mantiene contacto con el público. El equipo del Gobernador ejecuta con obediencia el trabajo y lo reporta al Gobernador. Si el Gobernador fuera a realizar todas las tareas por sí mismo, el no podría atender bien al público ni a la prensa. Esto sería parecido a una ventana que está realizando una tarea extensa en su hilo primario: no responde al usuario hasta que la tarea ha sido completada. Este programa podría beneficiarse con la creación de un hilo adicional que sería el respondable de la extensa tarea, permitiendo al hilo primario responder a las órdenes del usuario.

Podemos crear un hilo llamando a la función CreateThread que tiene la siguiente sintaxis:

CreateThread proto lpThreadAttributes:DWORD,\
                                dwStackSize:DWORD,\
                                lpStartAddress:DWORD,\
                                lpParameter:DWORD,\
                                dwCreationFlags:DWORD,\
                                lpThreadId:DWORD

La función CreateThread se parece un poco a CreateProcess.
lpThreadAttributes  --> Puedes usar NULL si quieres que el hilo tenga el manejador de seguridad por defecto.
dwStackSize --> especifica el tamaño de la pila del hilo. Si quieres que la pila del nuevo hilo tenga el mismo tamaño que la pila del hilo primario, usa NULL en este parámetro.
lpStartAddress--> Dirección de la función del hilo. Es la función que hará el trabajo del hilo. Esta función DEBE recibir uno y sólo un parámetro de 32-bits y regresar un valor de 32-bits.
lpParameter  --> El parámetro que quieres pasar a la función del hilo.
dwCreationFlags --> 0 significa que el hilo corre inmediatamante después de que es creado. Lo opuesto es la bandera CREATE_SUSPENDED.
lpThreadId --> La función CreateThread llenará el ID del hilo del nuevo hilo creado en esta dirección.

Si la llamada a CreateThread tiene éxito, regresa el manejador del hilo creado. Sino, regresa NULL.

La función del hilo corre tan pronto se realiza la llamada a CreateThread, a menos que especifiques la bandera CREATE_SUSPENDED en dwCreationFlags. En ese caso, el hilo es suspendido hasta que se llama a la función ResumeThread.

Cuando la función del hilo regresa con la instrucción ret, Windows llama a la función ExitThread para la función de hilo implícitamente. Tú mismo puedes llamar a la función ExitThread con tu función de hilo pero hay un pequeño punto qué considerar al hacer esto. Puedes regresar el código de salida del hilo llamando a la función GetExitCodeThread. Si quieres terminar un hilo desde otro, puedes llamar a la función TerminateThread. Pero sólo deberías usar esta función bajo circunstancias extremas ya que la función termina el hilo de inmediato sin darle chance al hilo de limpiarse después.

Ahora vamos a ver los métodos de comunicación entre los hilos.

Hay tres de ellos:

Los hilos comparten los recursos del proceso, incluyendo variables globales, así que los hilos pueden usar variables globales para comunicarse entre sí. Sin embargo este método debe ser usado con cuidado. La sincronización de hilos debe tenerse en cuenta. Por ejemplo, si dos hilos usan la misma estructura de 10 miembros, ¿qué ocurre cuando Windows de repente jala hacia sí el control de un hilo mientras éste estaba en medio de la actualización de la estructura? ¡El otro hilo quedará con datos inconsistentes en la estructura! No cometas ningún error, los programas multihilos son difíciles de depurar y de mantener. Este tipo de errores parecen ocurrir al azar lo cual es muy difícil de rastrear.

También puedes usar mensajes Windows para comunicar los hilos entre sí. Si todos los hilos son interface de usuario, no hay problema: este método puede ser usado como una comunicación en dos sentidos. Todo lo que tienes que hacer es definir uno o más mensajes de ventana hechos a la medida que sean significativos sólo para tus hilos. Defines un mensaje hecho a la medida usando el mensaje WM_USER como el valor base:

        WM_MYCUSTOMMSG equ WM_USER+100h

Windows no usará ningún valor desde WM_USER en adelante para sus propios mensajes, así que puedes usar el valor WM_USER y superiores como tus valores para los mensajes hechos a la medida.

Sin uno de los hilos es una interface de ususario y el otro es un obrero, no puedes usar este método como dos vías de comunicación ya que el hilo obrero no tiene su propia ventana, así que no posee una cola de mensajes. Puedes usar el siguiente esquema:

        Hilo de interface de usuario ------> variable(s) global(es)----> Hilo obrero
        Hilo obrero  ------> mensaje(s) de ventana hecho(s) a la medida----> Hilo de interface de usuario

En realidad, usaremos este método en nuestro ejemplo.
El último método de comunicación es un objeto de evento. Puedes concebir un objeto de evento como un tipo de bandera. Si el objeto evento es un estado "no señalado" [unsignalled], el hilo está durmiendo o es un durmiente, en este estado, el hilo no recibe una porción de tiempo del CPU. Cuando el objeto de evento está en estado "señalado" [signalled], Windows "despierta" el hilo e inicia la ejecución de la tarea asignada.

 

Ejemplo:

Deberías bajar el archivo zip y correr thread1.exe. Haz click sobre el elemento de menú "Savage Calculation". Esto le ordenará al programa que ejecute "add eax,eax " por 600,000,000 veces. Nota que durante ese tiempo, no puedes hacer nada con la ventana principal: no puedes moverla, no puedes activar su menú, etc. Cuando el cálculo se ha completado, aparece una caja de mensaje. Después de eso, la ventana acepta tus órdenes normalmente.

Para evitar este tipo de inconvenientes al usuario, podemos mover la rutoina de "cálculo" a un hilo obrero separado y dejar que el hilo primario continúe con su tarea de interface de usuario. Incluso puedes ver que aunque la ventana principal responde más lento que de costumbre,  todavía responde

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName  db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,OFFSET MenuName
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,300,200,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .WHILE TRUE
            invoke GetMessage, ADDR msg,NULL,0,0
            .BREAK .IF (!eax)
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
    .ENDW
    mov     eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .ELSEIF uMsg==WM_COMMAND
        mov eax,wParam
        .if lParam==0
            .if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        0,\
                                        ADDR ThreadID
                invoke CloseHandle,eax
            .else
                invoke DestroyWindow,hWnd
            .endif
        .endif
    .ELSEIF uMsg==WM_FINISH
        invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .ENDIF
    xor    eax,eax
    ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
        mov  ecx,600000000
Loop1:
        add  eax,eax
        dec  ecx
        jz   Get_out
        jmp  Loop1
Get_out:
        invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
        ret
ThreadProc ENDP

end start
 

Análisis:

El programa principal presenta al usuario una ventana normal con un menú. Si el usuario selecciona el elemento de menú "Create Thread", el programa crea un hilo a través del siguiente código:

            .if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        NULL,0,\
                                        ADDR ThreadID
                invoke CloseHandle,eax
 
La función de arriba crea un hilo que creará un procedimiento llamado ThreadProc que correrá concurrentemente con el hilo primario. Después de una llamada satisfactoria, CreateThread regresa de inmediato y ThreadProc comienza a correr. Puesto que no usamos manejadores de hilos, deberíamos cerrarlo, sino habrá cierta carencia de memoria. Nota que al cerrar el manejador del hilo éste no termina. El único efecto es que ya no se puede usar más el manejador del hilo.

ThreadProc PROC USES ecx Param:DWORD
        mov  ecx,600000000
Loop1:
        add  eax,eax
        dec  ecx
        jz   Get_out
        jmp  Loop1
Get_out:
        invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
        ret
ThreadProc ENDP

Como puedes ver, ThreadProc ejecuta un cáculo salvaje que tarda un poco para terminar y cuando finaliza envía un mensaje WM_FINISH a la ventana principal. WM_FINISH es nuestro mensaje hecho a la medida definido como:

no tienes que agregar WM_USER con 100h pero es más seguro hacerlo así.

El mensaje WM_FINISH es significativo sólo dentro del programa. Cuando la ventana principal recibe el mensaje WM_FINISH, responde desplegando una caja de mensaje que dice que el cálculo ha terminado.

Puedes crear varios hilos en sucesión enviando varias veces el mensaje "Create Thread".
En este ejemplo, la comuicación se realiza en un solo sentido ya que sólo un hilo puede notificar la ventana principal. Si quieres que el hilo principal envíe órdenes [commands] al hilo obrero, lo puedes hacer así:

Cuando el usuario selecciona el elemento "Kill Thread" del menú, el programa principal pondrá el valor TRUE en la bandera de mando. Cuando ThreadProc observa que el valor en la bandera de mando es TRUE, sale del bucle y regresa terminando entonces el hilo.

Indice

Siguiente

[Iczelion's Win32 Assembly HomePage]

n u M I T_o r's   Programming Page

Este tutorial, original de Iczelion, ha sido traducido por:   n u M I T_o r