In Xojo è possibile evitare di bloccare l’interfaccia, magari durante dei calcoli particolarmente lunghi, utilizzando i Thread. Vediamo le loro caratteristiche e quando utilizzarli.
Spesso capita di dover iniziare operazioni che sono abbastanza lunghe, ad esempio la lettura di un file, l’elaborazione di un’immagine o semplicemente una serie di operazioni che coinvolgono la rete. In questo caso lanciare la procedura tramite un pulsante, o un menu, può essere non particolarmente piacevole per l’utente perché si trova con un programma non che non può fare altro e non risponde ai comandi.
In questi casi l’uso dei Thread risolve molti problemi perché ci permette di lanciare più operazioni parallelamente.
In realtà, i Thread in Xojo non sono veri e propri thread nativi, per cui abbiamo si la possibilità di utilizzare per effettuare più operazioni, ma queste continuano ad essere eseguite, allo stato attuale, sempre sul singolo core.
Per avere operazioni realmente in parallelo e che utilizzino più core bisogna, sempre allo stato attuale, utilizzare sub applicazioni e gestire la comunicazione con queste. Ma questo sarà argomento di un prossimo post.
Torniamo ai Thread in Xojo. Pur con il limite del core singolo possono essere davvero utili in quanto ci permettono di avere un’interfaccia utente funzionante anche in presenza di elaborazioni impegnative.
Interfaccia Utente e Thread
Altro limite dei Thread, ma questa volta in comune con moltissimi altri ambienti, è l’impossibilità di accedere (in lettura e/o scrittura) all’interfaccia utente durante l’esecuzione del codice. Questo, che inizialmente può sembrare un limite, in realtà è una delle più importanti misure di sicurezza nell’elaborazione dei thread perché ci evita di avere conflitti importanti con l’uso dell’interfaccia.
Possiamo utilizzare, se necessario, dei timer per leggere i valori dall’interfaccia e modificare un parametro (come proprietà) del nostro Thread o per aggiornare l’interfaccia leggendo dei valori (proprietà) del Thread.
Chiaramente sta al tipo di progetto stabilire se è necessario proteggere queste letture/scritture in opportuni semafori o sezioni critiche (altro elemento interessante e importante presente nel linguaggio).
Stati del Thread
I Thread possono avere diversi stati, rappresentati dalla proprietà state, che rappresentano diverse situazioni:
NotRunning, che indica che il thread non è in esecuzione
Running, che indica che il thread è in esecuzione
Sleeping, che indica che il thread è stato in messo in pausa tramite l’istruzione Sleep
Suspended, che indica che il thread è stato messo in pausa, ad esempio in attesa di altre elaborazione tramite l’istruzione Suspend.
Questi stati ci permettono di controllare l’esecuzione del thread e anche di decidere come gestirne la prosecuzione.
Ad esempio, possiamo richiedere all’interno del codice del thread, delle risorse sulla rete, in questo caso sospenderemo il processo, immediatamente dopo la richiesta per poi riprenderlo una volta ottenuta la risposta; o attendere qualche millisecondo per effettuare una nuova lettura di un dato su un file.
Esecuzione
Essendo di tipo cooperativo, i thread condividono l’esecuzione con quello principale, ovvero dove risiede l’interfaccia utente. Questo vuol dire che una apposito scheduler si occupa di passare da uno all’altro; tipicamente agli estremi dei cicli o dei controlli condizionali. La conseguenza di questo è che il timing può non essere ideale in ogni situazione, ad esempio il comando Sleep, che serve a sospendere un thread per un certo numero di millisecondi, può non essere così preciso (o sempre costante): ma questo è abbastanza ovvio considerando la natura cooperativa dei thread e il fatto che, in ogni caso, questo non è un linguaggio in realtime.
Il codice da eseguire va inserito nell’evento Run del Thread (ovviamente questo non vuol dire tutto il codice, può essere la chiamata principale o la gestione generale della logica). Il metodo Run lancia l’esecuzione del thread stesso.
(Non è l’unico caso di omonimia tra eventi e metodi o proprietà in Xojo, ma in realtà essendo contesti diversi la cosa è chiara e anche utile per evitare di dover ricordare comandi diversi per situazioni analoghe per contesti diversi)
Come già detto, il codice non può accedere all’interfaccia direttamente, ma solo tramite proprietà, per cui sarà possibile impostare le proprietà che saranno utilizzate nel codice del thread prima di lanciare il metodo Run o bisogna utilizzare un sistema di suspend/resume per popolarle in modo opportuno.
Creare una sottoclasse del thread
Per gestire i casi tipici, come ad esempio popolare una serie di dati, elaborarli nel codice del thread e aggiornare l’interfaccia, può essere utile creare una sottoclasse della classe thread ed utilizzare questa per la maggior parte dei casi:
Realizzarla è semplice, basta creare una nuova classe, che possiamo chiamare timedThread, impostare come super la classe Thread e aggiungere due proprietà:
theTimer as Timer, privata, che servirà per la gestione dello stato del thread
stopMe as Boolean, pubblica, che servirà per chiudere gentilmente il thread in caso di necessità.
In genere è sempre bene avere un proprietà booleana di questo tipo, perché permette nel codice di chiudere il thread, magari annullando le azioni eseguite, o rilasciando correttamente le risorse, piuttosto che usare kill per terminare il thread.
Il nostro thread ha degli eventi specifici che andiamo a definire:
- Event beforeRun()
- che sarà chiamato prima di iniziare l’esecuzione, utile per impostare l’interfaccia (ad esempio disabilitare dei pulsanti.)
- Event afterRun()
- che sarò chiamato al termine dell’esecuzione, utile per impostare l’interfaccia (ad esempio per abilitare dei pulsanti.)
- Event showStatus()
- che sarà chiamato durante l’esecuzione e permetterà di leggere dei valori e mostrarli nell’interfaccia. L’ideale è creare apposite proprietà.
- Event codeToRun()
- Il codice del thread, analogo all’evento Run del Thread, qui nominato diversamente solo per non creare confusione.
Aggiungiamo i metodi per gestire l’aggiornamento dell’interfaccia:
Private Sub TimerAction(t as Timer) //Questo codice sarà utilizzato dal timer interno per aggiornare l'interfaccia //t non viene utilizzato #Pragma unused t //richiamiamo l'evento per aggiornare l'interfaccia showStatus End Sub Private Sub TimerPostAction(t as Timer) //Questo codice sarà chiamato al termine dell'esecuzione del thread //richiamiamo l'evento per aggiornare l'interfaccia showStatus //Rimuoviamo handler RemoveHandler t.action, WeakAddressOf TimerPostAction //Richiamiamo l'evento per indicare che il thread è terminato AfterRun End Sub |
L’ultimo metodo da aggiungere è quello dell’esecuzione, ovvero facciamo l’override del metodo run originale:
Public Sub run() //Richiamiamo l'evento pre esecuzione beforeRun //impostiamo il timer se non è già stato creato, qui è possibile cambiarne la frequenza //o leggerla da una proprietà importabile dall'IDE If theTimer=Nil Then theTimer=New timer theTimer.Mode=timer.ModeOff theTimer.Period=100 End If //Aggiungiamo l'handler per inviare i messaggi all'interfaccia utente If theTimer<>Nil Then AddHandler theTimer.action,WeakAddressOf TimerAction theTimer.Mode=theTimer.ModeMultiple End If stopMe=False // Ora possiamo chiamare il metodo originale per lanciare il nostro thread Super.run() End Sub |
L’ultima cosa da fare è definire l’evento di esecuzione (run) originale, che in pratica deve eseguire il codice e poi gestire la corretta chiusura del timer.
Sub Run() Handles Run //Lanciamo il nostro codice codeToRun //Al suo termine rimuoviamo l'handler originale del timer If theTimer<>Nil Then theTimer.Mode=Timer.ModeOff RemoveHandler theTimer.action,WeakAddressOf TimerAction End If //Sostituendolo con quello di fine esecuzione If theTimer<>Nil Then AddHandler theTimer.action,WeakAddressOf TimerPostAction theTimer.Mode=theTimer.ModeSingle Else AfterRun End If End Sub |
Ora abbiamo una classe thread compatibile con l’aggiornamento dell’interfaccia. Il tutto funziona perché i Timer vengono eseguiti a livello di thread principale per cui il timer inserito è adatto ad aggiornare l’interfaccia.
Chiaramente se abbiamo molti thread in esecuzione conviene una strategia diversa magari con un timer unico che legge i valori dai thread. Ma per la maggior parte dei casi questa classe risolve semplicemente i problemi dando anche qualche feature in più, come la possibilità di impostare l’interfaccia prima dell’avvio ( e magari copiare nel thread i dati rilevanti) e ripristinarla alla fine.
Chiudere i thread
Ci sono dei casi in cui abbiamo attivi dei thread in una finestra e vogliamo chiuderli quando chiudiamo la finestra o l’applicazione senza generare errori (infatti se un thread è attivo alla chiusura magari potrebbe accedere ad una risorsa che a causa della chiusura non è disponibile, generando quindi un errore).
Come possiamo fare. Ci sono diverse tecniche. Ne vediamo qui un paio con un esempio molto semplice.
Poniamo di aver definito una sottoclasse di thread, che chiamiamo mThread, che ha 3 proprietà (per semplicità tutte pubbliche, ma chiaramente questo è un esempio):
i as integer, che funge da contatore
stopMe as boolean, che come già accennato prima, usiamo per terminare in modo gentile il thread
stopTime as integer, che serve ad indicare il tempo per cui vogliamo sospendere momentaneamente il thread
Poi nell’evento run scriviamo questo codice:
i=0 //eseguiamo un ciclo fintanto che non lo fermiamo o raggiungiamo un valore limite While Not stopMe And i<10000 i=i+1 //Sospendiamo per qualche istante il thread Sleep stopTime Wend |
Chiaramente questo codice serve solo come simulazione di esecuzione.
Ora nella finestra, trasciniamo la nostra classe thread, chiamiamo l’istanza Thread1 e impostiamola come ControlSet, in modo da poterne creare diverse da codice.
Aggiungiamo una Label, con il nome Label1 e impostiamo anche questa come ControlSet.
Aggiungiamo un metodo Aggiorna, che si occuperà di aggiornare ogni Label1 al corrispondente valore i del rispettivo Thread1:
Public Sub update(idx as integer) Label1(idx).Text=Thread1(idx).i.ToText Label1(idx).TextColor=If(Thread1(idx).State=Thread.Running, Color.Green, Color.Red) End Sub |
Definiamo una costante numeroDiThread=5 (o qualsiasi altro valore maggiore di 0) per indicare quanti Thread vogliamo.
Trasciniamo un Timer e nel suo evento action inseriamo:
For i As Integer=0 To numeroDiThread update(i) Next |
In questo modo il timer aggiornerà i valori.
Aggiungiamo un metodo per avviare i thread:
Public Sub start() For i As Integer=0 To numeroDiThread Thread1(i).Run Next Timer1.Mode=Timer.ModeMultiple End Sub |
Ora possiamo definire il contenuto dell’evento open per fare in modo che all’apertura della finestra, vengano creati tanti thread e tante label come richiesto e tutti i thread vengano avviati automaticamente.
Sub Open() Handles Open //Il primo thread è già creato Thread1(0).stopTime=200 update(0) //Per gli altri li dobbiamo creare e diamo un tempo di stop maggiore in modo crescente For i As Integer=1 To nT Dim t As New Thread1 t.stopTime=Thread1(i-1).stopTime*2 Dim l As New Label1 l.top=l.top+32*i update(i) Next //Dopo aver presentato la finestra, avviamo i thread xojo.Core.Timer.CallLater 1000, WeakAddressOf start End Sub |
In questo modo all’avvio dell’applicazione tutti i thread si avviano e vediamo i valori crescere con le rispettive diverse velocità.
Ma cosa succede se chiudiamo la finestra?
Proviamo a farlo e andiamo nel debugger mettendo immediatamente in pausa il programma. Dal debugger nell’IDE vediamo che i nostri thread sono ancora li. Questo non è quello che vogliamo e ancora di più è uno spreco di risorse e potenzialmente un generatore di errori a runtime.
Come possiamo risolverlo?
Un metodo semplice ed elegante è quello di aggiungere l’evento cancelClose della finestra, un codice che si occupa di chiedere ai thread di fermarsi:
Function CancelClose(appQuitting as Boolean) Handles CancelClose as Boolean For i As Integer=0 To numeroDiThread //Per ogni thread se non è già fermo non è già in stop (per cui si fermerà da solo) If Thread1(i).state<>Thread.NotRunning and Thread1(i).stopIt=False Then //Digli di fermarsi Thread1(i).stopIt=True //Preparati a chiudere la finestra di nuovo xojo.Core.Timer.CallLater 1, WeakAddressOf autoChiudi //Blocca la chiusura della finestra Return True End If Next End Function |
Dove autoChiudi è semplicemente un metodo che ha come istruzione self.close per chiudere la finestra.
In questo modo se il thread non è già terminato (o non iniziato) e non è già candidato per fermarsi lo facciamo chiudere gentilmente e al suo termine libererà le risorse.
Volendo farlo a livello di app, con thread potenzialmente che provengono da diverse finestre possiamo usare la stessa strategia (quindi se appQuitting è True evitare il codice appena scritto) e chiudere se possibile gentilmente i thread o (visto che l’applicazione si sta chiudendo) semplicemente terminarli.
Un esempio di chiusura con terminazione dei Thread a livello di app è il seguente (sempre nell’evento cancelClose ma livello di app):
Function CancelClose() Handles CancelClose as Boolean //Utilizziamo l'oggetto runtime per vedere che oggetti sono ancora "vivi" a questo punto. Dim o As Runtime.ObjectIterator = Runtime.IterateObjects While o.MoveNext //Se l'oggetto corrente è un thread lo terminiamo If o.Current IsA Thread Then Thread(o.Current).Kill End If Wend End Function |
Questo solo a titolo d’esempio.
Riassumendo i thread possono permettere di eseguire più compiti in parallelo, li dobbiamo usare in modo indipendente dall’interfaccia utente, possiamo creare delle sottoclassi con proprietà ed eventi di comodo per semplificare la loro gestione.