Creare un demone in Qt

Premessa:

Un demone, in linux, è sostanzialmente un processo eseguito in background che offre dei servizi in maniera autonoma. Un processo in background, a differenza dei normali processi in foreground, non ha iterazione con l’utente, quindi non gestisce lo standard di input (ad esempio la tastiera) e lo standard di output (ad esempio in monitor), nonché il canale standard degli errori (ad esempio il terminale che ha avviato il processo). Il demone viene controllato dal sistema operativo per essere avviato e quindi arrestato in determinate fasi. Per fare questo è necessario corredarlo da uno script che verrà utilizzato dal kernel per il totale controllo del processo.

Scrivere un demone per linux:

Prima di passare alla creazione di un demone in Qt è necessario creare un demone indipendente dalla libreria. Il demone è compilabile solo in ambiente linux, ed in particolare, in questo esempio, per la distribuzione Debian e la sua derivata Ubuntu. Il demone è un programma normalissimo che parte nella funzione main, ma prima del corpo principale effettua una serie di istruzioni racchiuse per comodità nella funzione daemonize.

int main(int argc,char *argv[])
  {
  daemonize();
  
  // Core
  // ...
  // ...
  
  return(EXIT_SUCCESS);
  }

La funzione daemonize permette di effettuare le seguenti operazioni:

  • crea un fork del processo (una copia di se stesso) e termina quello principale. In questo modo il processo che viene chiamato dallo script controllato dal kernel termina immediatamente.
  • modifica la maschera di gestione dei file, permettendo al demone di operare con i massimi privilegi.
  • crea un nuovo sid nel sistema operativo altrimenti, visto che il padre è stato terminato, resterebbe un processo orfano.
  • è consigliabile collegare i segnali del sistema operativo di SIGTERM (quando viene richiesta la terminazione del processo) e SIGHUP (quando viene richiesta una ricarica della configurazione).
  • è consigliabile modificare la directory corrente in quella di root (perché si è certi che esiste), oppure in un’altra conosciuta.
void daemonize(void)
  {
  pid_t pid,sid;
  
  // Fork off the parent process
  pid=fork();
  if(pid<0)
    {
    exit(EXIT_FAILURE); // Fork error
    }

  if(pid>0)
    {
    exit(EXIT_SUCCESS); // This is a parent
    }

  // This is a child

  umask(0); // Change umask
  
  sid=setsid(); // New process group
  
  if(sid<0)
    {
    exit(EXIT_FAILURE);
    }
  
  // Connect the signals
  signal(SIGCHLD,SIG_IGN);
  signal(SIGTSTP,SIG_IGN);
  signal(SIGTTOU,SIG_IGN);
  signal(SIGTTIN,SIG_IGN);
  signal(SIGHUP,signalHandler);
  signal(SIGTERM,signalHandler);
  
  // Change current directory
  if((chdir("/"))<0)
    {
    exit(EXIT_FAILURE);
    }
  }

Dopo aver chiamato la funzione daemonize il nuovo processo è pronto per eseguire il codice vero e proprio.

Scrivere un demone per la libreria Qt:

Uno dei vantaggi della libreria Qt è quello della gestione degli eventi. Integrando la libreria Qt in un demone siamo in grado di sfruttare al meglio tutti i vantaggi di creare un processo in background controllato dal sistema operativo ma scritto con un eccellente libreria di supporto. Il primo passo è quello di creare una funzione engine che permetta di creare un’istanza di QCoreApplication (il demone non può avere interfaccia grafica), collegare i segnali del sistema operativo, istanziare il Core della nostra applicazione e quindi avviare il gestore eventi.

int engine(int argc,char *argv[])
  {
  QCoreApplication a(argc,argv);
  
  Core core;
  
  return(a.exec());
  }

Il main assumerà quindi questa forma:

int main(int argc,char *argv[])
  {
  daemonize();
  return(engine(argc,argv));
  }

Una volta preparato il demone si tratta quindi di eseguire un normalissimo programma Qt che però non interagisce con l’utente.

Se vogliamo conoscere la versione del demone e quindi avere un piccola iterazione con esso possiamo gestirlo parzialmente come un normale programma.

bool command(int argc,char *argv[])
  {
  if((argc==2)&&(QString(argv[1]).toLower()==QString("version")))
    {
    QTextStream stream(stdout);
    stream << "Daemon version X.Y.Z\n";
    stream.flush();
    return(true);
    }
  return(false);
  }

int main(int argc,char *argv[])
  {
  if(command(argc,argv))
    {
    return(EXIT_SUCCESS);
    }

  daemonize();
  return(engine(argc,argv));
  }

In questo modo se alla chiamata del processo si aggiunge il parametro version il demone si comporta come un programma normale, stampando a video la sua versione.

Controllo del demone:

Un demone per essere installato nel sistema operativo, in particolare con la distribuzione Debian, deve essere corredato da uno script di supporto. Lo script permette di gestire dei parametri che ne controllano il suo avvio, la sua terminazione, il riavvio, lo stato ed eventuali opzioni come per esempio la richiesta della versione.

#!/bin/bash

# Get lsb function
. /lib/lsb/init-functions

case "$1" in
  start)
    echo "Starting Daemon"
    if start-stop-daemon --start --quiet --oknodo --exec /opt/daemon/daemond; then
    echo "ok"
    else
    echo "error"
    fi
    ;;
  stop)
    echo "Stopping Daemon"
    if start-stop-daemon --stop --quiet --oknodo  --exec /opt/daemon/daemond; then
    echo "ok"
    else
    echo "error"
    fi
    ;;
  restart)
    echo "Restarting Daemon"
    $0 stop
    $0 start
    ;;
  version)
    /opt/daemon/daemond version
    ;;
  status)
    status_of_proc /opt/daemon/daemond daemon
    ;;
  *)
    echo "Usage: /etc/init.d/daemon {start|stop|restart|status}"
    ;;
esac

exit 0

Per migliorare la leggibilità è consigliabile aggiungere al nome del demone il suffisso d, mentre lo script lasciarlo con il nome originario. Per esempio il demone si potrebbe chiamare demoned mentre lo script solo demone.

Installazione:

Il demone prima di tutto deve essere copiato in una directory del sistema operativo, ad esempio /opt/daemon
Lo script invece deve essere copiato nella directory /etc/init.d con i permessi di esecuzione.
Lanciando con l’utente di root il comando ‘update-rc.d demone defaults’ viene installato lo script di controllo del demone nei vari stati del sistema operativo (avvio, funzionamento, arresto, ecc). Eseguendo il riavvio della macchina il demone viene avviato.

Per interagire con il demone, sempre con l’utenza di root, è necessario utilizzare il comando service seguito dal nome del demone e quindi dal parametro che verrà passato allo script.
Ad esempio:
service demone status

Conclusioni:

Poter creare un demone in linux è molto importante per elaborare servizi indipendenti dall’iterazione con l’utente, farlo in Qt è ancora più interessante. Una progettazione lungimirante permette di realizzare un software che potrà essere eseguito sia normalmente che come demone.

Esempio completo in Qt5

Condividere i thread in Qt

Premessa:

Si parla spesso di come condividere le risorse tra i thread ma raramente qualcuno si occupa di come condividere i thread come risorse. Può sembrare un gioco di parole, ma spesso capita che un thread gestisca una risorsa o dei servizi che possono essere utilizzati da più parti nel programma ed anche, magari, da thread differenti. In questo caso non basta prendere tutte le precauzioni per scambiare le informazioni, come abbiamo visto nel precedenti articoli, ma serve anche un sistema per assicurare che chi effettua le richieste ottenga i giusti risultati. Nell’esempio del precedente articolo, dove il thread permette di calcolare la media tra due numeri emettendo un signal con il risultato, è facile costatare che a fronte di due richieste della media da due parti distinte del programma non ci sia la possibilità di riconoscere a chi è indirizzato il risultato.

Identificare una richiesta in un thread:

Nel precedente articolo abbiamo visto come gestire i thread in Qt tramite il meccanismo dei signals/slots, un eccellente implementazione del pattern Observer. Questo meccanismo permette di chiamare in maniera asincrona delle funzioni (slots) e di ricevere dei segnali (signals) che possono essere collegati ad altrettante funzioni. Gli slots, per essere gestiti tra thread differenti e quindi essere asincroni, non devono avere dei dati di ritorno. Non è quindi possibile fare in modo che il thread assegni un identificativo univoco alla richiesta. Per farlo dobbiamo perciò inglobare nello slot un parametro che ci permetta, una volta ritornato identico dal signal del risultato, di riconoscere i risultati che ci appartengono.

Classe Ticket:

Una soluzione semplice ed efficace consiste nel creare una classe Ticket, ovviamente thread-safe, che possa essere generata in modo univoco e nello stesso tempo possa essere copiata mantenendo l’identità iniziale. Per l’univocità basta aggiungere una variabile che viene assegnata nel costruttore di default ad un contatore statico, mentre per mantenere l’identità è necessario implementare il costruttore e l’operatore di copia.

class Ticket
  {
  public:
    Ticket();
    Ticket(const Ticket &other);

    quint64 index(void) const;

    Ticket &operator=(const Ticket &other);
    bool operator==(const Ticket &other) const;
    bool operator!=(const Ticket &other) const;

  public:
    static void registerMetaType(void);

  private:
    quint64 m_index;
  };

Come abbiamo visto nell’articolo precedente, per passare la classe Ticket come parametro tra i thread questa deve essere dichiarata e registrata al meta-object-sistem, il sistema che si occupa di gestire il meccanismo dei signals/slot. Rimane valido il consiglio di registrare la classe Ticket nel costruttore della classe AbstractWorker.

Classe AbstractWorker:

Per poter identificare le richieste effettuate al thread è necessario che ogni slots sia provvisto del parametro ticket, cosi come il signal del risultato.

class AbstractWorker: public QObject
  {
    Q_OBJECT
  public:
    explicit AbstractWorker(QObject *parent=0);

  signals:
    void result(const Ticket &ticket,const CustomNumber &r);

  public slots:
    virtual void average(const Ticket &ticket,const CustomNumber &a,const CustomNumber &b)=0;
  };

Implementazione della classe Worker:

Il parametro ticket, passato nello slot, non serve a nulla nella classe Worker, però deve essere ricordato fino all’emissione del signal che ritorna i risultati dell’elaborazione.

Gestire il thread come risorsa:

Per chiamare uno slot del thread non basta passare un ticket, è necessario anche ricordarlo per essere in grado di riconoscere il signal che verrà emesso e quindi poter appropriarsi dei risultati richiesti. Una soluzione potrebbe essere quella di inserire il ticket in una lista in modo da poter verificare sia la sua presenza sia la sua identità a fronte della ricezione del signal.

void Core::run(void)
  {
  QTextStream stream(stdout);
  CustomNumber a("a",6);
  CustomNumber b("b",8);
  Ticket ticket;

  stream << "A: " << a.value() << " " << a.string() << "\n";
  stream << "B: " << b.value() << " " << b.string() << "\n";
  stream << "Ticket: " << ticket.index() << "\n\n";
  stream.flush();

  m_worker->average(ticket,a,b);
  m_ticket_list.append(ticket);
  }
void Core::workerResult(const Ticket &ticket,const CustomNumber &r)
  {
  QTextStream stream(stdout);

  if(m_ticket_list.contains(ticket))
    {
    m_ticket_list.removeAll(ticket);
    stream<<"Result value: "<< r.value() << " " << r.string() <<"\n";
    stream << "Ticket: " << ticket.index() << "\n\n";
    stream.flush();
    emit quit();
    }
  }

Conclusioni:

Quando si desidera utilizzare il thread per gestire una risorsa o delle operazioni condivise da più parti del processo è molto importante identificare le richieste per poter ricevere e quindi riconoscere i giusti risultati. Il meccanismo dei signals/slots non permette di identificare il mittente e nemmeno di emettere un signal per un particolare destinatario per cui la soluzione più pratica consiste nel utilizzare un ticket che permetta di seguire in modo univoco tutta le operazioni che vengono delegate al thread, dall’invio della richiesta alla ricezione del risultato. In questo modo saremo in grado di gestire in un thread separato una risorsa comune (magari lenta) oppure fornire dei servizi dedicati. Ritornando all’esempio dell’articolo sui thread, in una ditta è meglio avere poche persone preparate e specializzate a cui assegnare un lavoro e ricevere i risultati piuttosto che assumere ogni volta un dipendente per svolgere un lavoro e quindi licenziarlo al suo termine.

Esempio completo in Qt5

Comunicare tra i thread in Qt con classi personalizzate

 Premessa:

Il meccanismo dei signals/slots in Qt, come visto nel precedente articolo, è il metodo più semplice e funzionale per comunicare tra i thread. Se la classe che emette il signal e quella che lo riceve tramite slot sono su due thread differenti l’evento viene messo in una coda e quindi processato dal gestore eventi del thread ricevente. Tutti i parametri scambiati tra i signals e gli slots devono però essere conosciuti dalla Qt ed in particolare dal meta-object-system, che si occupa proprio della gestione dei signals/slots.

Creare una classe personalizzata:

Una classe, per essere utilizzata come parametro nei signals/slot collegati tra diversi thread, deve avere come minimo le seguenti caratteristiche:

  • un costruttore pubblico predefinito
  • un costruttore di copia pubblico
  • un distruttore pubblico

Inoltre la classe, per essere riconosciuta dal meta-object-system, deve essere dichiarata e quindi registrata. Come nell’esempio dell’articolo precedente, creiamo un thread che calcola la media tra due numeri, ma in questo caso ogni numero è incorporato in una classe personalizzata che ne contiene anche il nome.

#include <QMetaType>

class CustomNumber
  {
  public:
    CustomNumber(const QString &string=QString(),quint32 value=0);
    CustomNumber(const CustomNumber &other);
    ~CustomNumber();

    QString string(void) const;
    void setString(const QString &string);

    quint32 value(void) const;
    void setValue(quint32 value);

    CustomNumber &operator=(const CustomNumber &other);
    bool operator==(const CustomNumber &other) const;
    bool operator!=(const CustomNumber &other) const;

  public:
    static void registerMetaType(void);

  private:
    QString m_string;
    quint32 m_value;

  };

Q_DECLARE_METATYPE(CustomNumber)

La dichiarazione della classe avviene tramite la macro Q_DECLARE_METATYPE. E’ consigliabile mettere la macro di seguito alla dichiarazione della classe, nello stesso file header. La registrazione della classe deve avvenire a runtime prima di essere utilizzata. La soluzione migliore è quella di creare una funzione pubblica e statica all’interno della classe che registrerà se stessa una volta chiamata dall’esterno.

void CustomNumber::registerMetaType(void)
  {
  if(!QMetaType::type("CustomNumber"))
    {
    qRegisterMetaType<CustomNumber>("CustomNumber");
    }
  }

Chiamando questa funzione siamo sicuri che la classe verrà registrata correttamente ed univocamente.

Utilizzare una classe personalizzata:

Una volta implementata la classe personalizzata è necessario ricordarsi di registrarla prima di utilizzare il meccanismo dei signals/slots. Nel caso dei thread, se si utilizza il metodo spiegato nell’articolo precedente, la strada migliore per registrare la classe è quella di farlo nel costruttore della classe AbstractWorker. In questo modo si è sicuri che la classe personalizzata sia conosciuta al meta-object-system prima di essere utilizzata e nello stesso tempo la registrazione viene fatta una sola volta in un punto ben definito del codice. Se la classe personalizzata è utilizzata in più parti è consigliabile sempre eseguire la registrazione nei costruttori delle classi astratte dei worker in modo che il codice sia perfettamente riutilizzabile senza dipendere da classi esterne.

Conclusioni:

Dichiarando e registrando una classe personalizzata (che deve avere come minimo il costruttore pubblico di default, il costruttore di copia pubblico ed il distruttore pubblico) è possibile scambiare tramite signals e slots qualsiasi tipo di informazione rendendo l’utilizzo dei thread ancora più potente ma nello stesso tempo mantenendo la semplicità e la pulizia del programma.

Esempio completo sviluppato in Qt5

La strada migliore per gestire i thread in Qt

Premessa:

La gestione dei thread è considerata, da molti, l’aspetto più complesso della programmazione. Tuttavia, una buona progettazione del software permette di utilizzare i thread in modo del tutto naturale, superando velocemente gli ostacoli iniziali. Se fino a qualche anno fa i thread permettevano di realizzare programmi che non si bloccassero a fronte di operazioni lente, oggi, con l’avvento dei processori multicore, vengono migliorate anche le prestazioni permettendo di distribuire l’esecuzione delle istruzioni su più core fisici. Prima di entrare nel dettaglio su come gestire i thread in Qt è utile riassumere dei concetti basilari. Un processo è sostanzialmente un programma in esecuzione che, normalmente, viene avviato come singolo thread. Il thread è la parte operativa del processo, ne condivide la memoria ma non lo stack, i registri ed il program counter. Se il sistema operativo è multithreading durante l’esecuzione del processo ogni thread può create altri thread figli. Se l’hardware è multicore la contemporaneità dell’esecuzione viene suddivisa su più moduli fisici con conseguenti benefici in termini di prestazioni. Quindi, riassumendo, un processo è un’istanza di un programma al quale viene assegnato uno spazio di indirizzamento ed un thread principale, mentre i thread sono la parte operativa dei processi con un proprio stack, la situazione dei registri ed il program counter.

Thread nel mondo reale:

Per fare un esempio nel mondo reale l’esecuzione di un processo potrebbe essere paragonata all’apertura di un’azienda, con tanto di stabile ed un titolare che funge da thread principale. Se il carico di lavoro aumenta, il titolare assume una o più persone (thread) che occupano gli spazi dell’immobile (condividono la memoria) ed ai quali verrà delegato del lavoro. L’azienda è sempre gestita dal titolare ma i vari dipendenti possono svolgere altre mansioni e cooperare tra di loro per raggiungere dei risultati, condividendo risorse e scambiandosi informazioni. Con più persone che lavorano nello stesso luogo e con gli stessi strumenti è necessaria un’organizzazione chiara ed efficiente, per questo il personale (thread) sono la risorsa più difficile da gestire in un’azienda. Assegnare un lavoro e poi controllare continuamente lo svolgimento è totalmente inefficiente, per cui la fiducia è il primo pilastro su cui si fonda il lavoro di gruppo. Se un dipendente non serve più si può licenziare che equivale a terminare il thread. Nel caso di chiusura dell’azienda (processo) l’ultimo ad uscire è sempre il titolare.

I Thread nella progettazione del software:

I thread per essere efficienti devono lavorare in modo autonomo, eventualmente ricevendo comandi e rilasciando risultati. Tutto quello che viene svolto all’interno del thread è buona cosa che rimanga il più possibile indipendente dagli altri thread. Solitamente il thread principale del processo è quello che si interfaccia al sistema operativo, controlla tutti i suoi thread figli e gestisce l’interfaccia utente. I thread dello stesso processo condividono la stessa memoria per cui è necessario proteggerla dall’accesso concorrente. Questo è l’aspetto più difficile da comprendere, bisogna sempre ricordarsi che sono i dati che vanno protetti non le istruzioni. Ovviamente questo vale se più thread accedono agli stessi indirizzi, mentre se ogni thread alloca le proprie variabili senza condividerle non c’è la necessità di proteggere l’accesso. Ritornando all’esempio dell’azienda, se ogni dipendente ha i propri spazi di lavoro non deve condividerli con i colleghi risparmiando tempo nell’attesa che siano liberi (evitare l’accesso concorrente), di contro ci saranno molti spazi in azienda a volte inutilizzati (spreco di memoria). A volte i programmatori, una volta scoperti i thread, si lasciano prendere dall’entusiasmo e cominciano a crearne decine e decine, talvolta uno per ogni oggetto. Così come le persone, anche i thread occupano risorse, possono creare confusione e talvolta in gran numero posso peggiorare i tempi di lavoro e innalzare la possibilità di creare conflitti. Solo l’esperienza permetterà di decidere quando e quanti thread servono realmente in un processo.

I Thread in Qt:

La libreria Qt implementa il pattern Observer tramite il meccanismo dei signals and slots, un efficiente sistema per sincronizzare le informazioni tra più classi. Ogni classe che eredita da QObject può emettere degli eventi (signals) che, se collegati tra loro, possono essere ricevuti da appositi metodi (slots) della stessa o di un’altra classe per essere processati. Se la classe che emette il signal e quella che lo riceve tramite slot sono nello stesso thread, lo slot viene invocato immediatamente. Se i thread sono differenti l’evento viene messo in coda e quindi processato dal gestore eventi ricevente. In Qt i thread vengono gestiti tramite la classe QThread che, una volta istanziata, permette di avviare un ciclo di eventi in un nuovo thread. Questo ci permette di avere più gestori degli eventi che girano nello stesso processo. In Qt abbiamo l’enorme vantaggio di poter comunicare tra i thread con il meccanismo dei signals and slots con i parametri passati nativamente protetti dagli accessi concorrenti. Interfacciarsi alle classi solo tramite signals e slots è un metodo sicuro per evitare mal di testa con la protezione della memoria ed è un’ottima scuola sulla buona progettazione OOP. Vediamo ora come è possibile scambiare delle informazioni tra i thread in modo semplice ed efficace.

La classe AbstractWorker:

La prima cosa da fare quando vogliamo delegare del lavoro è quello di progettare una classe astratta che definisce il profilo dell’oggetto ideale che intendiamo creare, come fosse il profilo del lavoratore che vogliamo assumere in azienda. In questo modo, liberi dall’implementazione, possiamo definire i signals e gli slots che ci servono per soddisfare le nostre esigenze. Nell’esempio definiamo una classe che ci permette di calcolare la media tra due interi:

class AbstractWorker: public QObject
  {
    Q_OBJECT
  public:
    explicit AbstractWorker(QObject *parent=0);

  signals:
    void result(quint32 value);

  public slots:
    virtual void average(quint32 a,quint32 b)=0;
  };

Come potete vedere la classe astratta eredita da QObject, definisce un signal result e uno slot virtuale puro average. Nella realtà noi vorremmo assumere un lavoratore che, una volta assegnatogli due interi ci restituisca la media tra questi. Questa classe non può essere istanziata direttamente, è solo il profilo del nostro dipendente ideale.

Una volta definita la classe AbstractWorker possiamo progettare la classe Worker vera e propria.

class Worker: public AbstractWorker
  {
    Q_OBJECT
  public:
    explicit Worker(QObject *parent=0);

  public slots:
    virtual void average(quint32 a,quint32 b);
  };

La classe Worker eredita il signal result ed implementa la funzione average mantenendo l’interfaccia verso l’esterno definita nella classe astratta. Nell’esempio dell’azienda questa implementazione potrebbe equivalere ad ottenere tutte le procedure e capacità per effettuare il lavoro. La classe Worker può essere istanziata e utilizzata direttamente, ma questo equivale ad utilizzare risorse dello stesso thread, ovvero realizzare personalmente il lavoro. Al suo interno la classe Worker può definire funzioni, variabili e tutto quello che serve per soddisfare il lavoro richiesto.

Possiamo quindi creare un lavoratore autonomo creando un nuovo thread.

class Thread: public AbstractWorker
  {
    Q_OBJECT
  public:
    explicit Thread(QObject *parent=0);
    virtual ~Thread();

  public slots:
    virtual void average(quint32 a,quint32 b);

  private:
    QThread *m_thread;
    Worker *m_worker;
};

Come vedete la classe Thread eredita sempre da AbstractWorker in modo da mantenere inalterata l’interfaccia, però essa crea un lavoratore ed un nuovo thread al quale sarà affidata la parte operativa.
Istanziando la classe Thread avremo la stessa interfaccia di Worker ma con il vantaggio di eseguire le operazioni su un altro thread. Abbiamo assunto un nuovo dipendente per fare il lavoro che ci eravamo prefissati.

Progettando il software in questo modo siamo liberi addirittura con una define di decidere se istanziare direttamente la classe Worker (utilizzare lo stesso thread) o la classe Thread (creare un nuovo thread) mantenendo la stessa interfaccia.

#define CORE_USE_THREAD

AbstractWorker *m_worker;

#ifdef CORE_USE_THREAD
m_worker=new Thread(this);
#else
m_worker=new Worker(this);
#endif

Remmando la costante CORE_USE_THREAD possiamo instanziare la classe Worker sul thread principale.

Applicazione Qt con thread singolo:

La classe QCoreApplication gestisce il ciclo degli eventi del thread principale. Viene istanziata direttamente la classe Worker i cui slot sono sempre gestiti dal thread principale.

Applicazione Qt multithread:

La classe QCoreApplication gestisce sempre il ciclo degli eventi del thread principale. Istanziando però la classe Thread viene creato un nuovo thread che gestisce tutti gli eventi della classe Worker. Come potete vedere in questo modo la classe Worker rimane una classe a se stante che comunica con il thread principale esclusivamente tramite i signals e slots della classe Thread. Ereditanto entrambi dalla classe astratta sia la classe Thread che la classe Worker forniscono la stessa interfaccia per cui sono interscambiabili. Questo è il metodo più efficiente per gestire un thread in Qt, sia a livello di sviluppo che di progettazione.

Vantaggi e svantaggi nell’utilizzo dei thread

Pro:

– Anche su processori monocore, l’utilizzo di più thread rende i programmi più fluidi ed utilizzabili anche di fronte ad operazioni molto lente.
– Viene sfruttata tutta la potenza delle macchine moderne, che vantano più core.

Contro:

– Se i thread utilizzati sono superiori ai core fisici del processore il tempo totale di esecuzione di un processo aumenta rispetto ad un thread per ogni core per la gestione della schedulazione.
– Progettare un software multithread è molto più complesso di uno a thread singolo.

Cenno al multitasking:

Nonostante con i thread sia possibile creare delle grandi applicazione mono processo, se il progetto assume dimensioni consistenti ed è seguito da più persone è consigliabile dividerlo in processi diversi. Una volta definita una logica di comunicazione (ad esempio tramite socket) con delle chiare specifiche, ogni singolo team potrà occuparsi del proprio processo ignorando quello che fanno gli altri. Ci sono diversi vantaggi ad operare in questo modo: la possibilità di utilizzare diversi compilatori, il fatto di compilare solo il processo necessario, la capacità di mantenere un processo di dimensioni adeguate che potrà essere gestito anche da un solo sviluppatore ed infine la possibilità di far girare i processi anche su macchine differenti. Ritornando all’esempio dell’azienda, se questa aumenta di dimensioni è consigliabile costruire un nuovo stabile (processo) piuttosto che espandere quello esistente in maniera eccessiva.

Conclusioni:

Imparare la gestione dei Thread obbliga a spendere più tempo in progettazione per identificare ruoli ed interfacce di ogni oggetto. Questo si traduce poi in un’implementazione pulita e ordinata del codice con indubbi vantaggi nel mantenimento dello stesso. Il risultato tangibile sarà poi l’esperienza utente nell’utilizzo del programma e le prestazioni rispetto al codice tradizionale. Senza dubbio il consiglio è quello di cominciare con un thread per gestire una risorsa lenta o comunque accessibile da più parti, ad esempio l’accesso ad file.

Esempio completo sviluppato in Qt5