domenica 26 marzo 2017

Multi-threading e interfaccia grafica GTK+

Il multi-threading è la suddivisione di un processo in diversi sotto processi eseguiti in parallelo (o concorrentemente), questo ci permette di eseguire il nostro programma sfruttando la potenza di calcolo di tutti i core presenti nei moderni processori dotati di tecnologia multi-core.

Questa tecnica è molto utile soprattutto per migliorare le prestazioni in presenza di un carico di lavoro pesante, ma è anche utilizzata per preservare la reattività dell'interfaccia grafica in qualsiasi situazione.
Mentre negli anni ottanta era accettabile il fatto di dover attendere davanti al monitor mentre il computer finiva di elaborare l'operazione assegnata, al giorno d'oggi, trovarsi davanti ad un'interfaccia grafica che non risponde a nessun comando ci farebbe subito pensare che il software in esecuzione sia andato in crash o che comunque ci sia un problema.

Quando non è possibile velocizzare un'operazione al punto da renderla quasi istantanea, allora bisogna intrattenere l'utente per ingannare l'attesa. Ad esempio, se l'interfaccia grafica fornisce informazioni sullo stato dell'esecuzione di un'operazione complessa, l'utente attenderà sapendo che il software sta lavorando correttamente.

Iniziamo con il creare una classe che sia in grado di svolgere un compito su un thread separato dal programma principale.



E' una classe molto semplice: ha una variabile membro di tipo double chiamata fraction_done, che ci servirà a simulare l'esecuzione di un lavoro, ed una denominata wrk_mutex, che è di tipo mutex (dall'inglese mutual exclusion, mutua esclusione) fornito dalla standard library per impedire che più thread accedano contemporaneamente agli stessi dati.

Le funzioni membro sono solo due: start_work() che esegue il lavoro e get_fraction_done() che comunica lo stato dell'esecuzione.

All'interno della funzione start_work() troviamo un primo blocco, delimitato dalle parentesi graffe, all'interno del quale si invoca la classe lock_guard, che serve ad acquisire il mutex ed impedire a qualsiasi altro thread in esecuzione di interferire fino alla fine delle parentesi. All'interno di questo blocco protetto, fraction_done è inizializzato con il valore di 0.0.
Successivamente troviamo un loop infinito, all'interno del quale il thread in esecuzione attende 250 millisecondi con la chiamata di funzione sleep_for(), per simulare lo svolgimento di un'operazione complessa.
Quindi c'è un nuovo blocco di codice con una nuova acquisizione del mutex e si incrementa il valore di fraction_done, infine verifica se il lavoro è concluso (al raggiungimento del valore di 1.0) ed esce dal loop.

Ora scriviamo un piccolo programma che utilizza la nostra classe Worker.


Nella funzione main() viene dapprima creata un'istanza della classe Worker, quindi viene creato un nuovo thread che esegue la funzione membro start_work() di Worker. Da questo punto in poi il programma principale è eseguito in modo concorrenziale a start_work(), nel loop infinito all'interno di main() viene richiesto all'istanza di Worker il valore attuale di fraction_done.

Nella funzione get_fraction_done() viene invocata la classe lock_guard per impedire che fraction_done sia modificato mentre se ne comunica il valore.

Quindi viene stampato sul video il valore ottenuto (solo se diverso dall'ultimo ottenuto).
Se il valore ha raggiunto 1.0 allora esce dal loop perché il lavoro è terminato, infine viene invocata la funzione join() per riunire il thread al programma principale prima di rilasciare le risorse allocate e terminare.

Proviamo a compilare ed eseguire


Funziona come volevamo, il thread svolge il suo compito ed il programma principale ci tiene aggiornati sullo stato del lavoro.
Ora proviamo ad applicare la stessa tecnica ad un programma con interfaccia grafica realizzata con la libreria GTK+.

Utilizzerò l'IDE Anjuta per realizzare un nuovo programma GTK+.

Creiamo un nuovo progetto di tipo C++ GTKmm (semplice).
Nelle opzioni del progetto spuntiamo la voce "configura pacchetti esterni"
Spuntiamo il pacchetto pthread-stubs
Dopo aver creato il progetto andiamo a creare l'interfaccia grafica con Glade


Come potete vedere dall'immagine è una finestra con una barra progressiva ed un'etichetta per visualizzare lo stato del lavoro. Subito sotto ci sono i pulsanti per avviare e fermare il thread.

Ora modifichiamo la classe Worker per farla lavorare in un programma dotato di interfaccia grafica.


Analizzando l'header potete vedere l'aggiunta di alcune variabili membro:
- has_stopped - ci servirà per capire se il thread è fermo;
- shall_stop - utilizzato per comunicare al thread l'intenzione di fermarlo;
- caller_notification - questo è un signal che utilizzeremo per comunicare con il programma principale.

Inoltre sono stati aggiunte tre funzioni:
- has_stopped_working() - per sapere se il thread è fermo;
- stop_work() - per fermare il thread;
- signal_caller_notification() - per connettere il signal al nostro programma.

Per finire è stata modificata la funzione start_work(), con l'aggiunta dell'emissione del signal e la possibilità di fermare il lavoro tramite shall_stop.

Ora vediamo il programma principale.


Oltre al file main.cc ho creato una classe Controller che si occupa di gestire l'intera applicazione.
Di solito si trovano esempi o tutorial in C++ che utilizzano GTKmm dove si crea una classe derivata di Gtk::Window e si costruisce l'interfaccia da codice, il mio approccio è differente.
Per prima cosa preferisco creare l'interfaccia grafica con Glade, non vedo il vantaggio di costruirla tramite codice, per quanto riguarda questo pattern, è probabilmente frutto dalle precedenti esperienze di programmazione su OS X con Objective-C.

L'istanza di Controller creata in main() si occupa di recuperare gli elementi dell'interfaccia grafica dal file .ui, quindi in connect_signals() collega i signal dei pulsanti e di due altri oggetti: ctrl_worker e ctrl_dispatcher.
Il primo è un'istanza della classe Worker e collega il suo signal alla funzione on_worker_notification().
Il secondo è un oggetto della classe Gtk::Dispatcher ed è l'elemento fondamentale per garantire il corretto funzionamento del thread di Worker con l'interfaccia del programma principale.
Alla pressione del pulsante start viene creato un thread al quale viene affidata l'esecuzione della funzione start_work() di Worker, il puntatore denominato wrk_thread ne terrà traccia.
Ogni qualvolta il thread emette un signal questo verrà ricevuto dal programma ed eseguirà la funzione on_worker_notification() che, a sua volta, emetterà il signal del nostro ctrl_dispatcher.
Quest'ultimo verrà ricevuto dalla funzione on_dispatcher_notification() che si occuperà di verificare lo stato del thread, aggiornare l'interfaccia grafica ed eliminarlo se fermo.

Questo è il programma perfettamente funzionante.


Provando a non utilizzare l'istanza di Gtk::Dispatcher, collegando il signal di Worker direttamente alla funzione che gestisce il thread ed aggiorna l'interfaccia grafica, finiremmo con un crash dell'applicazione in un momento non precisato dell'esecuzione.


Nessun commento:

Posta un commento