Archivio mensile:ottobre 2015

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