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: |
|
| Language: | Italian |
| Comments: | Formattazione Wiki: Antelox |
Introduzione
In questo tutorial impareremo come creare un programma multithread. Studieremo inoltre i metodi di comunicazione tra i threads.
Tools
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:
- 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.
- 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:
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:
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:
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!
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':
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.
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:
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.