![]() |
Developer AssistantProgrammazione Multi-Threading |
Questa sezione ha come scopo di presentare alcune caratteristiche e modalità d'uso dei thread. Successivamente potrei presentare alcuni esempi (molto semplici) di programmazione multithreading. Sotto OS/2 il multitasking è gestito in una meniera molto versatile tramite due concetti di processo: la sessione e i thread. Ogni volta che viene lanciato un programma viene aperta una nuova sessione, cioè viene creato tutto l'ambiente necessario perché questo possa funzionare. Una sessione si distingue per uno spazio di memoria individuale, semafori propri, pipes, ed ogni tipo di risorsa che non apprtenga al SO. Appena creata la sessione viene lanciato il primo thread del programma che a sua volta, se il programma è multi-threaded, può lanciare altri thread. Un thread è assai più semplice della sessione in quanto tutti i thread in una sessione condividono lo stesso spazio di indirizzamento, DLL, memoria condivisa (shared memory) e semafori. Un thread è fondamentalmente una porzione del programma che possiede un proprio stack e viene eseguita in maniera asincrona rispetto agli altri thread nella stessa sessione. Gli utilizzi dei thread sono veramente infiniti e possono migliorare notevolmente il rendimento di un programma, in quanto permettono di sfruttare tutto il tempo che altrimenti verrebbe speso in attese di eventi esterni. Proprio a causa di questa flessibilità dei threads è molto importante pianificare correttamente sin dall'inizio dello sviluppo dei nostri progetti quali parti del programma potranno diventare dei thread indipendenti. Candidate ideali per diventare thread separati sono funzioni di input output da porte seriali o funzioni complesse che non interagiscono con dispositivi di ingresso/uscita (con l'unica eccezione dei dischi rigidi) e che richiedono diverso tempo per giungere a termine. Per chi programma in Presentation Manager i thread sono molto importanti perché sono l'unico sistema veramente efficente per evitare che l'interfaccia grafica possa bloccarsi durante il processing dei messaggi. Una buona regola di programmazione sotto PM è quella di assegnare un thread a tutti quei messaggi il cui trattamento richiederebbe un tempo maggiore di 0.1s; in tal modo si assicura che la coda dei messaggi non venga mai bloccata dal nostro programma. Uno dei messaggi più soggetti a questo problema è proprio il WM_PAINT in quanto l'operazione di ridisegnare l'intera finestra richiede spesso ben più di 0.1s. Comunque in questa mia descrizione ho intenzione di parlare di thread in senso generico e quindi non aggiungerò niente di specifico per PM. Come ho già precedentemente accennato tutti i thread all'interno di un processo vengono eseguiti contemporaneamente in maniera asincrona e quindi per comunicare tra un thread e l'altro è necessario tener ben presente che non si può fare praticamente alcuna assunzione sull'esecuzione di un thread se non si sono predisposti alcuni sistemi di sicronizzazione. Uno dei sistemi più semplici, ma anche estremamente rudimentale, per sincronizzare due thread consiste nel far uso di una variabile globale che agisce come semaforo. Molto spesso è assai più conveniente usare le strutture fornite dal SO come: pipes, queues, semaphores, shared memory o files. Rimando la discussione di queste strutture ad un eventuale altro articolo per ora accontentatevi di vedere l'uso di un semaforo per coordinare i due thread usati nel programma d'esempio. Queste operazioni di sincronizzazione tra thread vengono anche chiamate serializzazione delle risorse. Non richiedono srializzazione le funzioni "reentrant" che di solito vengono indicate nella documentazione delle librerie. Nella maggior parte dei compilatori per OS/2 esitono due funzioni dedicate alla creazione e alla conclusione dei thread: _beginthread e _endthread (In molti casi queste funzioni si trovano solo nelle librerie dedicate allo sviluppo di programmi multithreaded). Queste due funzioni sono molto particolari in quanto non solo creano o distruggono un thread, ma si preoccupano di gestire i semafori e le variabili statiche usate dalle funzioni di libreria in modo da garantire che una funzione che richiede serializzazione (printf per esempio) non venga usata da più thread in contemporanea, altrimenti si rischierebbe di ottenere risultati molto diversi da quelli attesi. È possibilissimo creare e distruggere thread con le usuali API di OS/2 (DosCreateThread & DosExit), ma in tal modo le librerie del C non sono più affidabili dal punto di vista della serializzazione in quanto non sono state create le strutture necessarie. Se si vuole usare le API per gestire i thread è necessario gestirsi autonomamente la serializzazione delle funzioni di libreria e in tal caso non conviene usare le librerie multithreaded, ma è necessario fare molta attenzione nell'utilizzo delle risorse comuni ai vari thread.
Tra le varie funzioni svolte da _beginthread c'è anche la creazione di una copia delle variabili globali di libreria (come errno) specifica per ogni thread di modo che il loro stato non venga modificato da altri threads. Nel caso si faccia uso di tali variabili diventa assai più difficile usare direttamente le API di OS/2. Apparentemente la cosa sembra abbastanza semplice: o si usa _beginthread e _endthread lasciando alle librerie il compito della serializzazione delle funzioni di libreria (ma gestendosi autonomamente le proprie risorse comuni ai vari thread) oppure si usano le API di OS/2 gestendo la serializzazione di tutte le funzioni che lo richiedono. I problemi sorgono quando si vuole agire in maniera asincrona su un thread tramite le funzioni di libreria come: DosKillThread o DosSuspendThread. La prima di queste due funzioni è forse la meno pericolosa anche se crea dei problemi, la seconda invece può bloccare completamente la nostra applicazione.
Affrontiamo prima i problemi legati a DosKillThread.
Veniamo quindi a DosSuspendThread.
Arriviamo quindi alla parte più interessante: la pratica.
Consiglio di leggere questo listato cercando di capirlo bene in ogni particolare. Le API di OS/2 sono spiegate abbastanza bene nella documentazione del toolkit, comunque sono disponibile per eventuali chiarimenti.
Può essere interessante vedere come si comporta il programma quando si preme Ctrl-Break durante l'esecuzione. L'installazione di un gestore delle eccezioni non è indispensabile, ma è assai consigliabile per tenere sotto controllo gli errori nell'esecuzione del thread. È importante ricordarsi di rimuovere il gestore al termine del thread perché altrimenti si rischia di mandare in crisi il meccanismo di gestione delle eccezioni (le volte che mi è capitato di non farlo al termine del thread principale si verificava una eccezione XCPT_BAD_STACK). Vediamo ora lo stesso esempio realizzato con le librerie multi-threded e le funzioni _beginthread() e _endthread(). Listato 2A causa del meccanismo di serializzazione delle librerie multi-thread questo programma può comportarsi in maniera diversa ogni volta che viene eseguito, quindi consiglio di eseguirlo più volte per notare i vari comportamentei possibili. A questo punto mi fermo perché l'argomento è vastissimo ed ogni ulteriore approfondimento dovrebbe per forza considerare un caso particolare; quindi se nessuno manifesta un interesse per una specifica applicazione concludo qui. | |
![]() |
Last modified 8-1-97 |