Zoom Icon

Tutorial 15: Programmazione in Multithreading

From UIC

Tutorial 15: Programmazione in Multithreading

Contents


Tutorial 15: Programmazione in Multithreading
Author: Iczelion
Email: Traduttore: -NeuRaL_NoiSE
Website: Mirror
Date: 11/01/2009 (dd/mm/yyyy)
Level: Slightly hard
Language: Italian Flag Italian.gif
Comments: Formattazione Wiki: Antelox


Introduzione

In questo tutorial impareremo come creare un programma multithread. Studieremo inoltre i metodi di comunicazione tra i threads.


Tools

Esempio


Preliminari

Nel tutorial precedente, avete imparato che un processo contiene almeno un thread: il thread primario (primary thread). Un thread e' una catena di esecuzione. Potete anche creare threads addizionali nel vostro programma. Potete considerare il multithreading come un multitasking all'interno di un solo programma. In termini di implementazione, un thread e' una funzione che viene eseguita in concomitanza col programma principale. Potete eseguire diverse istanze della stessa funzione o potete eseguire diverse funzioni simultaneamente, a seconda delle vostre necessita'. Il multithreading e' proprio di Win32, non esistono equivalenti in Win16. I threads vengono eseguiti nello stesso processo, cosicche' possono accedere a qualsiasi risorsa del processo, come variabili globali, handles, ecc. Comunque, ogni thread ha la sua propria stack cosicche' le variabili locali in ciascun thread sono private. Ogni thread possiede inoltre il suo set privato di registri, di modo che quando Windows passa ad altri threads, il thread puo' "ricordarsi" il suo ultimo stato e puo' "riprendere" la funzione che stava espletando quando riottiene il controllo. Cio' viene gestito internamente da Windows. Possiamo dividere i threads in due caterorie:

  1. User interface thread: Questo tipo di thread crea la sua propria finestra cosicche' puo' ricevere messaggi di windows. Puo' rispondere all'utente tramite la propria finestra, e da qui proviene il nome. Questo tipo di thread e' soggetto alla regola Win16 Mutex che consente un solo user interface thread in user e gdi kernel a 16-bit. Mentre uno user interface thread sta eseguendo codice dello user o del gdi kernel a 16-bit, nessun altro UI thread puo' servirsi dello user o del gdi kernel a 16-bit. Notate che questa Win16 Mutex e' valida per Windows 95 poiche' in profondita', le funzioni API di Windows 95 si spezzettano in codice a 16-bit. Windows NT non ha Win16 Mutex percio' gli user interface threads sotto NT funzionano molto piu' liberamente che sotto Windows 95.
  2. Worker thread: Questo tipo di thread non crea una finestra, e percio' non puo' ricevere messaggi di windows. Esiste fondamentalmente per eseguire il lavoro assegnato in background, e da qui il nome "worker thread".

Io consiglio la seguente strategia nell'utilizzo delle capacita' di multithreading di Win32: Lasciate che il thread primario esegua i compiti legati all'interfaccia utente e lasciate agli altri threads il compito di sbrigare i lavori pesanti in background. In tal modo, il thread primario e' come un Governatore, e gli altri threads sono lo staff del Governatore. Il Governatore assegna lavori allo staff, mentre egli mantiene il contatto con il pubblico. Lo staff esegue ubbidientemente il lavoro e poi ritorna dal Governatore. Se il Governatore avesse dovuto eseguire ciascun compito da solo, non sarebbe stato in grado di prestare molta attenzione al pubblico o alla stampa. Cio' e' quello che avviene ad una finestra impegnata ad eseguire un lungo lavoro nel suo thread primario: non risponde ai comandi dell'utente finche' il lavoro non e' completo. Un tale programma puo' beneficiare della creazione di un thread addizionale che e' responsabile dell'esecuzione del lungo lavoro, e che permette al thread primario di rispondere ai comandi dell'utente. Possiamo creare un thread chiamando la funzione CreateThread che ha la seguente sintassi:

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,  // indirizzo degli attributi di sicurezza del thread
    DWORD  dwStackSize,                        // dimensione inziale della stack del thread, in bytes
    LPTHREAD_START_ROUTINE  lpStartAddress,  // indirizzo della thread function
    LPVOID  lpParameter,                                                // argomento per il nuovo thread
    DWORD  dwCreationFlags,         // flags di creazione
    LPDWORD  lpThreadId            // indirizzo dell'identificatore di thread (thread ID) restituito
   );

La funzione CreateThread assomiglia moltissimo a CreateProcess.

lpThreadAttributes --> Potete usare NULL se volete che il thread abbia il descrittore di sicurezza predefinito.

dwStackSize --> Specifica la dimensione della stack del thread. Se volete che il thread abbia la stessa grandezza di stack del thread primario, usate NULL in questo parametro.

lpStartAddress --> Indirizzo della thread function. E' la funzione che eseguira' il lavoro del thread. Questa funzione DEVE ricevere uno e soltanto un parametro a 32-bit e restituire un valore a 32-bit.

lpParameter --> Il parametro che volete passare alla thread function.

dwCreationFlags --> Uguali a quelle nella funzione CreateProcess.

lpThreadId --> La funzione CreateThread riempira', a questo indirizzo, la thread ID del thread appena creato.

Se la chiamata a CreateThread ha successo, restituisce l'handle del thread appena creato. Altrimenti, restituisce NULL. La thread function viene eseguita appena la chiamata a CreateThread ha successo, a meno che voi non specifichiate la flag CREATE_SUSPENDED in dwCreationFlags. In tal caso, il thread e' sospeso finche' non viene chiamata la funzione ResumeThread. Quando la thread function ritorna con l'istruzione ret, Windows chiama implicitamente la funzione ExitThread per la thread function. Potete chiamare anche voi la funzione ExitThread all'interno della vostra thread function, ma farlo non ha molto senso. Potete rilevare l'exit code chiamando la funzione GetExitCodeThread. Se volete terminare un thread da un altro thread, potete chiamare la funzione TerminateThread. Ma dovreste usare questa funzione in circostanze estreme, poiche' essa termina immediatamente il thread, senza dargli alcuna possibilita' di eliminare i suoi "resti".

Ora spostiamoci sui metodi di comunicazione tra i threads. Ce ne sono tre:

  • Usando variabili globali;
  • Messaggi di Windows;
  • Eventi.

I threads condividono le risorse del processo, incluse le variabili globali, percio' essi possono utilizzarle per comunicare l'un l'altro. Comunque questo metodo deve essere utilizzato con molta attenzione. Bisogna prendere in considerazione la sincronizzazione dei threads. Ad esempio, se due threads usano la stessa struttura di 10 membri, che succede se Windows improvvisamente toglie il controllo ad uno dei threads proprio mentre stava aggiornando la struttura ? L'altro thread sara' lasciato con dei dati inconsistenti in una struttura! Non commettete errori, i programmi che usano il multithreading sono piu' difficili da "debuggare" e da mantenere. Questo tipo di bug sembra capitare casualmente, il che rende il problema molto difficile da intercettare. Potete usare inoltre i messaggi di Windows per comunicare tra i threads. Se sono tutti user interface threads, non c'e' problema: questo metodo puo' essere usato come un mezzo di comunicazione biunivoco. Tutto cio' che dovete fare e' definire uno o piu' messaggi personalizzati che hanno significato per i threads. Definite un messaggio personalizzato utilizzando il messaggio WM_USER come valore base, ad esempio:

WM_MYCUSTOMMSG equ WM_USER+100h

Windows non usera' nessun valore da WM_USER a salire per i propri messaggi, quindi potete usare il valore WM_USER e piu' per i valori dei vostri messaggi predefiniti. Se uno dei threads e' uno user interface thread e l'altro e' un worker, non potete usare questo metodo come sistema di comunicazione biunivoco, dal momento che un worker thread non ha la sua finestra e quindi nemmeno una coda di messaggi. Potete invece usare il seguente schema:

User interface Thread ------> variabile/i globale/i ----> Worker thread
Worker Thread ------> messaggio/i Windows personalizzati ----> User interface Thread

Infatti, useremo questo sistema nel nostro esempio. L'ultimo metodo di comunicazione e' l'event object. Potete considerare l'event object come una specie di flag. Se l'event object e' in stato "non-segnalato", il thread e' inattivo o "addormentato", e, in questo stato, il thread non ricevera' CPU time slice. Quando l'event object e' in stato "segnalato", Windows "sveglia" il thread e questo comincia ad eseguire il compito a lui assegnato.


Essay

Dovreste scaricare il file zippato con l'esempio e far partire thread1.exe. Clickate sul menu item "Savage Calculation". Cio' dira' al programma di eseguire "add eax,eax " per 600,000,000 di volte. Notate che durante questo tempo, non potete fare niente con la finestra principale: non potete muoverla, non potete attivarne i menu ecc. Quando il calcolo e' completato, una message box appare. Dopo di cio' la finestra accetta i vostri comandi normalmente. Per evitare questo tipo di inconveniente per l'utente, possiamo muovere la routine di "calcolo" in un worker thread separato e lasciare che il thread primario continui con il suo compito di interfaccia. Potete vedere che anche se la finestra principale risponde piu' lentamente del solito, comunque risponde!

include windows.inc
includelib user32.lib
includelib kernel32.lib
includelib gdi32.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
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
    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  hInstance
    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,0
    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
    mov   eax,uMsg
    .IF eax==WM_DESTROY
        invoke PostQuitMessage,NULL
    .ELSEIF eax==WM_COMMAND
        mov eax,wParam
        .if lParam==0
            .if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        NULL,NORMAL_PRIORITY_CLASS,\
                                        ADDR ThreadID
                invoke CloseHandle,eax
            .else
                invoke DestroyWindow,hWnd
            .endif
        .endif
    .ELSEIF eax==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


Analisi

Il programma principale si presenta all'utente con una normale finestra con un menu. Se l'utente sceglie il menu item "Create Thread", il programma crea un thread cosi':

.if ax==IDM_CREATE_THREAD
                mov  eax,OFFSET ThreadProc
                invoke CreateThread,NULL,NULL,eax,\
                                        NULL,NORMAL_PRIORITY_CLASS,\
                                        ADDR ThreadID
                invoke CloseHandle,eax

La funzione sovrastante crea un thread che eseguira' una procedura chiamata ThreadProc assieme al thread primario. Dopo che la chiamata ha avuto successo, CreateThread ritorna immediatamente e ThreadProc comincia ad essere eseguita. Poiche' non usiamo handle di thread, dovremmo chiuderlo o ci saranno perdite di memoria.

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

Come potete vedere, ThreadProc esegue una savage calculation che richiede un bel po' di tempo, e quando ha finito manda un messaggio WM_FINISH alla finestra principale. WM_FINISH e' il nostro messaggio personalizzato, definito come segue:

WM_FINISH equ WM_USER+100h

Non dovete per forza aggiungere 100h a WM_USER ma cosi' state piu' sicuri. Il messaggio WM_FINISH e' significativo solo all'interno del nostro programma. Quando la finestra principale riceve il messaggio WM_FINISH, risponde mostrando una message box che ci informa che il calcolo e' stato completato. Potete creare piu' threads in successione usando "Create Thread" diverse volte. In questo esempio, la comunicazione e' univoca perche' solo il thread puo' notificare qualcosa alla finestra principale. Se volete che il thread primario sia in grado di mandare comandi al worker thread, potete farlo cosi':

  • aggiungete un menu item che dice qualcosa come "Kill Thread" al menu.
  • aggiungete un variabile globale che viene usata come command flag. TRUE=Ferma il thread, FALSE=Continua il thread
  • Modificate ThreadProc e inserite nel loop un check del valore della command flag.

Quando l'utente sceglie il menu item "Kill Thread", il programma principale settera' il valore TRUE nella command flag. Quando ThreadProc rileva che il valore della command flag e' TRUE, esce dal loop e ritorna, terminando il thread.


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.