Qt: qthread, qtconcurrent e divagazioni

di Andrea Zani, in Qt,

Avevo iniziato a scrivere questo post molti anni fa... - in effetti il file salvato con parte di questo test era datato 2010. L'ho rispolverato dopo una discussione via mail riguardante proprio il framework Qt. Alcune domande che mi ero state poste facevano parte di questo post, dunque ho preferito rispolverarlo e portarlo a termine, e poi quando ho tempo mi fa sempre piacere scrivere codice con questo framework. Altra ragione per cui mi sono sentito obbligato a scriverlo è per delle inesattezze scritte nel post precedente a riguardo la app per il salvataggio delle immagini dalla webcam.

Vorrei partire però con calma con il giusto approccio iniziale quando si sviluppa con questo framework. Ecco il classico programma di esempio per la visualizzazione del messaggio "Hello world" - classico esempio che da vent'anni perseguita qualsiasi tutorial di base su qualsiasi linguaggio di programmazione.

// main.cpp
#include <stdio>

int main(int argc, char *argv[])
{
std::cout << "Hello world";
return 0;
} 

Ovviamente oltre all'oscenità dell'esempio, per la gestione di questo framework e per capire come funziona questo mondo, il programma qui sopra dovrebbe essere scritto in questo modo:

// main.cpp
#include <QCoreApplication>

#include "simplethread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    simpleThread st;
    QObject::connect(&st, SIGNAL(finished()), &a, SLOT(quit()));
    st.start(); // <- start() NOT run()!!!!!!!!
    return a.exec();
}

// simplethread.h
#ifndef SIMPLETHREAD_H
#define SIMPLETHREAD_H

#include <QThread>

#include <iostream>


class simpleThread : public QThread
{
    Q_OBJECT
public:
    explicit simpleThread(QObject *parent = 0);
private:
    void run();
};
#endif // SIMPLETHREAD_H

// simplethread.cpp
#include "simplethread.h"

simpleThread::simpleThread(QObject *parent) :
    QThread(parent)
{
}

void simpleThread::run()
{
    // Body is here
    std::cout<<"Hello world"<<std::endl;
}

Iniziando dal file "main.cpp", l'utilizzo dell'oggetto QCoreApplication è alla base delle applicazioni che vogliono utilizzare gli oggetti del framework Qt - per il mondo delle GUI, va utilizzata la classe QApplication. Queste devono essere utilizzare per la gestione corretta degli eventi sia del sistema che del nostro stesso applicativo. Come spiegato nel post precedente, alla base degli eventi del mondo Qt ci sono i signal & slot - mostrati anche nell'esempio appena scritto, che permettono una gestione facilitata ma differente dalla tipica gestione degli eventi. Nel file main.cpp si istanzia la classe QCoreApplication, quindi si crea un'istanza della classe simplethread. La riga successiva è la più importante - QObject::connect - e mostra in modo chiaro l'utilizzo degli eventi all'interno del mondo Qt: si collega l'evento finished del thread simplethread allo slot quit della classe QCoreApplication. La riga successiva avvia effettivamente il thread sottostante, fino ad arrivare alla riga di codice più importante:

a.exec();

Questo metodo avvia effettivamente la gestione degli eventi all'interno del mondo Qt. Senza di essa pure gli eventi, che ripeto sono gestiti grazie agli signal and slot, NON possono essere elaborati e gestiti. Da questo punto del codice, il thread principale del nostro semplice applicativo, rimane in attesa solo della gestione degli eventi che non si fermano solo a questi, ma anche anche alla gestione dei timer o degli altri oggetti asincroni. Quest'attesa termina solo quando viene comunicato ad "a" di uscire da questo loop infinito. La comunicazione avviene quando viene richiamato lo slot "quit" o "terminate" dell'oggetto QCoreApplication e, come spiegato sopra, questo viene richiamato grazie al signal del thread sottostante alla termine della sua esecuzione. Importante a questo punto ripetere il punto più importante di questo esempio: senza exec() non potranno essere gestiti gli eventi. Se il codice del file main.cpp fosse così:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    simpleThread st;
    QObject::connect(&st, SIGNAL(finished()), &a, SLOT(quit()));
    st.start(); // <- start() NOT run()!!!!!!!!
    QThread::sleep(5);
    return a.exec();
}

A schermo viene visualizzata correttamente e immediatamente la stringa "Hello world", ma anche se la classe simpleThread ha terminato il suo compito, lo sleep dopo lo start blocca il thread principale e l'evento quit non può essere elaborato fino a al termine dei cinque secondi e l'esecuzione dell'istruzione exec(). Tra poco vedremo un altro esempio a riguardo a questo problema quando parleremo di timer. Ma prima di andare maggiormente nei dettagli, ritorniamo alla causa principale di questo post: riscrivere in modo decente il codice mostrato in quel post. Vediamo come dovrebbe essere scritto, semplificando l'esempio della webcam sostituendolo con una banale scritta a video. Creato in Qt Creator un nuovo progetto di tipo "Qt Console Application", inseriamo in main.cpp questo codice:

#include <iostream>

#include <QCoreApplication>

#include "waitkey.h"
#include "counteraz.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    waitKeys wk;
    QObject::connect(&wk, SIGNAL(finished()), &a, SLOT(quit()));
    wk.start(); // START NOT RUN!!!

    counterAZ cz;
    QObject::connect(&wk, SIGNAL(finished()), &cz, SLOT(quit()));
    cz.start(); // START NOT RUN!!

    int result = a.exec();

    cz.wait();
    wk.wait();

    return result;
}

Non cambia molto dal codice mostrato precedentemente: viene instanziato l'oggetto QCoreApplication, quindi si creano due istanze di classe, una per la classe counterAZ e la per la classe waitKeys. Come nell'esempio precedente viene collegato il signal "finished" di una classe agli slot quit della classe principale del nostro esempio e della seconda classe: in questo modo forziamo l'uscita dal programma in modo corretto.

Nel dettaglio la classe waitKeys è utilizzata per notificare all'applicativo la pressione del tasto invio, e counterAZ per visualizzazione temporizzata di un messaggio a video (che nell'esempio precedente prendeva e salvava su disco un'immagine da internet).

#waitkey.h
#ifndef WAITKEYS_H
#define WAITKEYS_H

#include <QThread>

#include <iostream>

#include <QTextStream>


class waitKeys : public QThread
{
    Q_OBJECT
public:
    explicit waitKeys(QThread *parent = 0);

private:
    void run();
};

#endif // WAITKEYS_H

#waitkey.cpp
#include "waitkey.h"

waitKeys::waitKeys(QThread *parent) :
    QThread(parent)
{
}

void waitKeys::run()
{
    std::cout << "Press 'ENTER' to exit... ";
    QTextStream stream(stdin);
    stream.readLine();
// appena l'utente preme il testo invio questo thread
// termina e viene eseguito automaticamente il signal "quit".
}

Non c'è molto da dire: questa classe eredita da QThread perché deve essere eseguita in un suo thread. Così come la classe counterAZ:

// counterAZ.h
#ifndef COUNTERAZ_H
#define COUNTERAZ_H

#include <QThread>

#include <iostream>

#include <QTimer>


class counterAZ : public QThread
{
    Q_OBJECT
public:
    explicit counterAZ(QObject *parent = 0);
    void run();

public slots:
    void writeZ();
};

#endif // COUNTERAZ_H

// counterAZ.cpp
#include "counteraz.h"

counterAZ::counterAZ(QObject *parent) :
    QThread(parent)
{
}

void counterAZ::run()
{
        QTimer timer;
        QObject::connect(&timer, SIGNAL(timeout()), this, SLOT(writeZ()));
        timer.start(300);
        exec(); // <- event loop
}

void counterAZ::writeZ()
{
    std::cout<< "timer!"<<std::endl;
}

In questo esempio viene utilizzato QTimer. Così come il Timer del framework .net, serve per avviare a intervalli prestabiliti una nostra funzione. Ormai addentro alla metodologia del mondo Qt, anche in questo caso per specificare la funzione da richiamare dal timer, si usano i signal e slot - nel caso del timer, il signal timeout viene richiamato allo scadere dell'intervallo prestabilito. E la riga di codice con "exec()"? Come spiegato prima, essa serve per avviare nel thread attuale la gestione degli eventi e di tutti gli oggetti asincroni come QTimer. Che cosa accadrebbe senza la riga di codice con "exec"? Semplicemente il thread terminerebbe subito provocando anche la distruzione dell'oggetto QTimer. E se la funzione run contenesse uno sleep al suo posto?

void counterAZ::run()
{
        QTimer timer;
        QObject::connect(&timer, SIGNAL(timeout()), this, SLOT(writeZ()));
        timer.start(300);
        // forever is a macro: for(;;)
        forever
        {
           sleep(5);
        }
}

Abituati al mondo del framework .net, la risposta più logica potrebbe essere che il tutto funzionerebbe come l'esempio più sopra. Invece, da come si potrebbe aver capito dalla spiegazione sopra l'esempio, il timer non sarà mai eseguito. Inoltre alla pressione del tasto "Invio", il codice continuerà a girare all'infinito per il semplice motivo che lo slot "quit" è in grado solo di interrompere l'eventloop di sistema di exec. Per chiudere con violenza il thread si dovrebbe modificare il codice in questo modo:

QObject::connect(&wk, SIGNAL(finished()), &cz, SLOT(terminate()));

terminate blocca immediatamente il thread, sconsigliato in ogni caso perché è solo grazie al quit che possiamo prendere provvedimenti come la chiusura e la distruzione di oggetti e risorse utilizzate.

L'esempio più corretto, come sto cercando di far passare dopo questo fiume di parole, è con l'utilizzo del timer... ok, ma perché evitare lo sleep in un ciclo infinito? In fondo è una critica sensata, perché la funzione run girerebbe perfettamente anche in questo modo:

void counterAZ::run()
{
        forever
        {
            writeZ();
            this->msleep(400);
        }
}

Ma come detto sopra, questo preclude l'uso di timer e altri oggetti asincroni, e la forzatura della chiusura con il terminate. Non basta?

Provo a complicare un po' tutta la questione con un altro esempio:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    std::cout<<"Start..."<<std::endl;

    waitKeys wk;
    QObject::connect(&wk, SIGNAL(finished()), &a, SLOT(quit()));
    wk.start();

    counterAZ cz(true);
    QObject::connect(&wk, SIGNAL(finished()), &cz, SLOT(quit()));
    cz.start();

    QThread::sleep(5); // <- wait 5 seconds
    int result = a.exec();
...

E la funzione writeZ in modo che visualizzi la data con il dettaglio dei millisecondi:

void counterAZ::writeZ()
{    _messageToShow=QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
    std::cout<< _messageToShow.toStdString()<<std::endl;
}

E' uguale all'esempio sopra, però prima dell'avvio della funzione per la gestione dell'event loop viene forzata una pausa di dieci secondi. Domanda: che cosa accade una volta avviato l'applicativo? Giustamente non accade niente per dieci secondi ma il QTimer non è stato inattivo, lui allo start avvia ugualmente i suoi eventi con l'intervallo prestabilito, ma questi vengono concatenati nell'event loop del thread principale senza poter essere eseguiti. All'esecuzione di a.exec() dopo lo sleep, saranno eseguiti immediatamente tutti gli eventi in coda in quel momento e non quando il timer avvia l'evento:

esempio 1

Mi sto scaldando, ampio un po' il discorso. L'utilizzo dell'oggetto QThread non è l'unico utilizzabile all'interno dei nostri programmi scritti in c++ con l'utilizzo di Qt. Abbiamo due altri oggetti utilizzabili: QtConcurrent e QtRunnable. Il secondo non l'ho mai usato e lo ritengo inutile quindi userò qualche parole solo per il primo. Quest'oggetto - QtConcurrent - cerca di semplificare l'utilizzo di thread e comunicazione tra i vari livelli dell'applicazione.

#include <QCoreApplication>

#include <QtConcurrent/QtConcurrent>

#include <QFuture>

#include <iostream>

#include <QFutureWatcher>


int elaborate(int i)
{
    QThread::sleep(3); // Simulate slow function
    return 12*i;
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QFutureWatcher<int> watcher;
    QObject::connect(&watcher, SIGNAL(finished()), &a, SLOT(quit()));
    QFuture<int> futurex = QtConcurrent::run(elabora, 3);
    watcher.setFuture(futurex);
    a.exec();
    futurex.waitForFinished();
    int result = futurex.result();
    std::cout<<"Result: "<<result <<std::endl;
    return 0;
}

Via velocemente con l'ormai conosciuto "QCoreApplication", ecco che viene creato un oggetto QFuture con il tipo ritornato dalla funzione:

QFuture<int> futurex = QtConcurrent::run(elaborate, 3);

Elaborate è la funzione che sarà richiamato in un thread separato, al quale sarà passato il parametro int(3). L'oggetto watcher viene usato per la comunicazione tra l'oggetto QFuture con qualsiasi altro oggetto della nostra applicazione grazie al solito gioco di signal e slot. Ed ecco la domanda a tradimento: in quale riga di codice viene avviato effettivamente il nuovo thread? Se la risposta è alla riga "a.exec"... bravi... è sbagliato. Solo il timer e eventi richiamati da altri oggetti iniziano il loro lavoro all'avvio dell'event loop del thread attuale. Essendo un thread esterno, parte alla riga QtConcurrent::run. L'uso del watcher e del future permette l'uso corretto dell'event loop all'interno del nostro applicativo.

Ho già detto che sono in tutti tre gli oggetti per l'uso dei thread che parte dal più complesso e completo QThread - con il quale possiamo decidere anche la priorità del thread - passando per il QRunning fino al QConcurrent che permette anche giochetti su collection.

Senza cercare scopi estremi di questi oggetti, vediamo subito uno scopo in cui ci può essere utile. Per chi proviene dal mondo framework .net, avrà ben presente il funzionamento delle window application. Prendendo l'esempio classico delle windows forms sarà chiaro a chiunque è andato al di là di "Hello world" che la window principale dell'applicativo lavora in un unico thread, e la pressione su un button che richiama una procedura lunga bloccherebbe l'aggiornamento della stessa finestra fino alla fine dell'elaborazione. Ecco, lo stesso avviene nel mondo di Qt. Ed ecco un modo per utilizzare questi oggetti: è troppo facile avendo una soluzione trovare il problema che si addice che avendo il problema trovare la soluzione migliore... Il mondo informatico là fuori, purtroppo, si sposa perfettamente con la seconda visione.

Creiamo una mini windows application in Qt:

esempio 2

Semplice: due coppie di textbox e button. Ogni button aggiorna il text sopra con la data e ora attuali. La differenza è che nel primo caso la scrittura della data è diretta forzando uno sleep con l'apposita istruzione, mentre nel secondo caso la simulazione di un'elaborazione pesante avviene sempre con lo sleep, ma con l'uso di QtConcurrent. Ecco il codice ridotto dell'inutile:

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include <QDateTime>

#include <QtConcurrent/QtConcurrent>

#include <QFuture>

#include <QFutureWatcher>

#include <QThread>


namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
    
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    
private slots:
    void on_btn1_clicked(); // <- evento button1
    void on_bt2_clicked(); // <- evento button2
    void writex(); // <- write string
    QString getGuiDateTime(); // <- get last datetime

private:
    Ui::MainWindow *ui;
    QFuture<QString> futurex;
    QFutureWatcher<QString> watcher;
};

#endif // MAINWINDOW_H

// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btn1_clicked()
{
    QThread::sleep(5);
    ui->txt1->setText(QDateTime::currentDateTime().toString());
}

void MainWindow::on_bt2_clicked()
{
    ui->bt2->setEnabled(false);
    ui->txt2->clear();
    disconnect(&watcher, SIGNAL(finished()),0,0);
    connect(&watcher, SIGNAL(finished()), this, SLOT(writex()));
    futurex = QtConcurrent::run(this, &MainWindow::getGuiDateTime);
    watcher.setFuture(futurex);
}

void MainWindow::writex()
{
    QString value=futurex.result();
    ui->txt2->setText(value);
    ui->bt2->setEnabled(true);
}

QString MainWindow::getGuiDateTime()
{
    QThread::sleep(5);
    return QString("AZ: '%1'").arg(QDateTime::currentDateTime().toString());
}

Ok, semplice, il primo pulsante richiama la funzione on_btn1_clicked che dopo uno sleep di cinque secondi visualizza nel primo textbox la date e ora attuale. Una volta avviato questo programma e cliccato su questo pulsante, la finestra sarà bloccata completamente (senza possibilità di cliccare altro) e solo dopo il tempo prestabilito sarà visualizzato il messaggio e risarà dato il controllo all'utente. L'evento per il secondo pulsante è molto più interessante. Tutto parte dall'evento on_bt2_clicked. Si disabilità il pulsante e si cancella il contenuto della textbox nelle due prime righe. Di seguito viene sganciata la connessione tra il watcher e il signal del QtConcurrent. Questo è necessario perché ogni volta che aggiungiamo una connessione tra un singal e uno slot, queste non si sostituiscono a quello presente, ma viene aggiunto - così come avviene anche nel mondo del Framework .net con i delegate ed eventi. Infine viene avviato l'evento fittizio con la lunga elaborazione "getGuiDateTime". Riecco la pausa di cinque secondi, quindi ritorna la stringa da visualizzare nella textbox. A questo punto il watcher, finito il lavoro, fa scattare il signal "finished" che, essendo collegato allo slot writex, richiamerà questo metodo. Per prendere la stringa elaborata si richiama il metodo "result()" di future. Ora il testo e pronto per essere visualizzato e quest'ultima funzione, writex, inserisce il testo nella textbox e riabilita il pulsante. Questa volta, quando l'utente cliccherà su questo pulsante, la finestra rimarrà completamente funzionante, e sarà anche possibile cliccare ancora sul primo pulsante.

Così come il Framework .net si è inventato nel lontano 2005 un metalinguaggio per la creazione delle interfacce - wpf - pure in Qt si sono creati un metalinguaggio con lo stesso scopo: Qml. In questo caso si tratta di una struttura simile al Json, e da esso deriva anche il linguaggio javascript con cui possiamo interagire con gli oggetti della window. Inoltre, sempre da javascript, possiamo richiamare funzioni in C++ e Qt per avere la massima potenza di elaborazione. Mi permetto di dire che si tratta di una specie di comunicazione client <-> server, dove il client è una windows form definita in Qml i cui eventi e funzioni di base possono essere scritti direttamente al suo interno in javascript, ed è possibile richiamare funzioni pesanti sul server scritte in C++.

Ecco la banale interfaccia disegnata in Qml:

esempio 3

Ecco il codice:

import QtQuick 2.0
Item {
    id: item
    width: 300; height: 200

    signal qmlSignal(string msg)
    function showData(msg)
    {
        text1.text="From C++: "+msg;
        windowbutton1.enabled=true;
    }

    Rectangle {
        id: rootAll
        width: 300
        height: 200
        border.width: 12

        Rectangle
        {
            id: textContainer
            x: 34
            y: 39
            width: 175
            height: 20
            color: "#3bb72b"
            border.width: 2
            border.color:"red"

            TextEdit {
                id: text_edit1
                width: 170
                height: 17
                text: "test"
                anchors.verticalCenterOffset: 1
                anchors.horizontalCenterOffset: 0
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.verticalCenter: parent.verticalCenter
                color: "black"
                smooth: true
                clip: false
                cursorVisible: true
            }
        }

        Text {
            id: text1
            x: 34
            y: 66
            text: qsTr("Test to check qml output")
            font.pixelSize: 12
        }
        WindowButton {
            id: windowbutton1
            x: 167
            y: 154
            width: 112
            height: 24
            objectName: "btnData"
            function callback()
            {
                text1.text="";
                windowbutton1.enabled=false;
                item.qmlSignal(text_edit1.text)
            }
        }
    }
}

Si può suddividere in due categorie il codice qui presente: tag di visualizzazione e di codice. I primi sono quelli che sono utilizzati per la mera visualizzazione degli oggetti all'interno della nostra windows forms: Item e Rectangle, con le loro proprietà come la dimensione, colori, assi cartesiani per dove inserire i vari oggetti e così via; il codice sono le righe dove sono inseriti gli eventi in javascript, come le righe che iniziano con "function". Si possono oltresì suddividere in funzioni collegati direttamente ai singoli oggetti, come all'interno dell'oggetto WindowButton la funzione "callback", e funzioni utilizzabili da tutti gli oggetti della window: showData. Questa funzione è preceduta da questa riga di codice:

signal qmlSignal(string msg)

Essa è utilizzata per collegare il mondo Qml con il C++/Qt che vedremo tra poco. Prima di andare nello specifico ancora alcune parole su Qml e il codice appena visto. Si è parlato poco fa dell'oggetto windowButton. Questo non è un oggetto nativo di Qml, ma un nostro oggetto (che io ho preso da un tutorial online dedicato al Qml). Ecco il codice:

// windowButton.qml
import QtQuick 2.0

Image
{
    id: button

    Text
    {
        text: "Click me!"
    }
    signal qmlSignal()
    MouseArea
    {
        anchors.fill: parent
        id: mouseArea
        onClicked: callback()
    }
    states:[
        State
        {
            name:"hovered"
            when: mouseArea.pressed
            PropertyChanges { target: button; opacity: 1 }
        },
        State
        {
            name:"normal"
            when: mouseArea.pressed == false
            PropertyChanges { target: button; opacity: 0.7}
        }
    ]
    Behavior on opacity
    {
        NumberAnimation {duration:100}
    }
}

Oltre a quanto già visto prima, qui è interessante la sezione MouseArea in cui viene dichiarata la zona cliccabile e quale evento richiamare una volta che l'utente clicca con il mouse al suo interno (in questo caso viene richiamata la funzione callBack, presente nel file di codice precedente). Inoltre con li "State" possiamo definire alcuni comportamenti dell'oggetto; nell'esempio sopra viene cambiata l'opacità del pulsante se viene cliccato o meno l'oggetto. Ma come viene incluso questo file, windowButton.qml all'interno del primo file se non c'è nessuna dichiarazione che specifica l'importazione? La soluzione è semplice: come si può vedere anche in questo secondo file non è mai dichiarato in alcun modo il nome windowButton perché questo viene preso direttamente dal nome del file; quindi è semplice ora comprendere che è sufficiente inserirli nella stessa directory del file qml principale, per potervi accedere come oggetti. Innanzitutto si deve dichiarare nel file nomeprogetto.pro che vogliamo utilizzare nella nostra applicazione il Qml:

#-------------------------------------------------
#
# Project created by QtCreator
#
#-------------------------------------------------

QT       += core gui quick

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = QmlTest2
TEMPLATE = app



SOURCES += main.cpp     myclass.cpp

HEADERS  +=     myclass.h

OTHER_FILES +=     testx1.qml     WindowButton.qml

Ecco il codice del main.cpp:

#include <QObject>

#include <QApplication>

#include <QQuickView>

#include <QQuickItem>

#include "myclass.h"
#include <QDebug>


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QQuickView *view = new QQuickView;
#ifdef QT_DEBUG
    view->setSource(QUrl::fromLocalFile("C:/Users/andrewz/Documents/cpiupiu/QmlTest2/testx1.qml"));
#else
    view->setSource(QUrl::fromLocalFile(QApplication::applicationDirPath() + "/testx1.qml"));
#endif
    QObject *item = view->rootObject();
    MyClass myClass(item);
    QObject::connect(item, SIGNAL(qmlSignal(QString)), &myClass, SLOT(cppSlot(QString)));
    view->show();
    int returnValue = a.exec();
    delete view;
    return returnValue;
}

Se si è letto con un minimo di attenzione fino a questo punto, molte parti del codice qui sopra saranno facilmente comprensibili: creato l'oggetto QApplication per la gestione degli eventi nella nostra applicazione e QQuickView per la visualizzazione della windows form definita in Qml. Quindi viene creato l'oggetto fittizio item (fittizio perché sarà usato solo per lo scambio di informazioni tra il C++ e il Qml) e la classe "myClass" dove sono presenti le funzioni che il nostro codice Qml può richiamare. Ecco ancora l'uso del signal e degli slot, in questo caso il signal "qmlSignal" non è presente nel codice c++, ma nel file di codice qml principale visto prima:

signal qmlSignal(string msg)

cppSlot, invece, è una funzione presente in myClass. Vediamo ora tutto il codice di myClass:

//myClass.h
#ifndef MYCLASS_H
#define MYCLASS_H

#include <QObject>

#include <QQuickItem>

#include <QtConcurrent/QtConcurrent>

#include <QFuture>

#include <QFutureWatcher>


class MyClass : public QObject
{
    Q_OBJECT
public:
    explicit MyClass(QObject *item, QObject *parent = 0);
private:
    QObject *_item;
    QFuture<QString&qt; futurex;
    QFutureWatcher<QString> watcher;
    
signals:
    
public slots:
    void cppSlot(const QString &msg);
    void writex();
    QString getGuiDateTime(QString msg);
};

#endif // MYCLASS_H

// myClass.cpp
#include "myclass.h"

MyClass::MyClass(QObject *item, QObject *parent) :
    _item(item),QObject(parent)
{
}

void MyClass::cppSlot(const QString &msg)
{
    disconnect(&watcher, SIGNAL(finished()),0,0);
    connect(&watcher, SIGNAL(finished()), this, SLOT(writex()));
    futurex = QtConcurrent::run(this, &MyClass::getGuiDateTime,msg);
    watcher.setFuture(futurex);
}

void MyClass::writex()
{
    QString value=futurex.result();
    QVariant returnValue;
    QMetaObject::invokeMethod(_item,"showData", Qt::DirectConnection,
                              Q_RETURN_ARG(QVariant, returnValue),
                              Q_ARG(QVariant, value));
}

QString MyClass::getGuiDateTime(QString msg)
{
    QThread::sleep(5);
    return QString("%1: '%2'").arg(msg).arg(QDateTime::currentDateTime().toString());
}

Interessante a questo punto il codice con cui eseguiamo due operazioni distinte: permettere a Qml di richiamare una funzione in C++, e permettere al nostro codice C++ di richiamare una funzione presente in Qml. Naturalmente per non farci mancare niente, l'uso di QtConcurrent per lasciare che la nostra window non sia freezata durante l'elaborazione fittizia. A questo punto, avviando l'applicativo, nella finestra vista sopra, abbiamo una textbox (definita in Qml con il tag TextEdit), una label (definita in Qml con il tag Text) e un button custom spiegato in precedenza. Premendo sul button nel codice qml viene richiamato l'evento callback che esegue queste operazioni (con le spiegazione a fianco):

text1.text=""; <- cancella label text
windowbutton1.enabled=false; <- disabilita pulsante
item.qmlSignal(text_edit1.text) <- fa scattare il signal "qmlSignal" passando come parametro il contenuto della textbox.

Richiamato "qmlSignal" scatta di conseguenza lo slot collegato, questa volta lato "server", cioè la funzione "cppSlot" in mainClass. All'interno di questa funzione viene istanziato e configurato l'oggetto QtConcurrent come visto nell'esempio della classica windows form. Alla fine dell'elaborazione, quando viene eseguita la funzione "writex", con questo codice richiamiamo la funzione javascript "showData" all'interno di Qml:

    QString value=futurex.result(); // <- legge valore
    QVariant returnValue; // <- return value fittizio
    QMetaObject::invokeMethod(_item,"showData", Qt::DirectConnection,
                              Q_RETURN_ARG(QVariant, returnValue),
                              Q_ARG(QVariant, value));
// richiama funzione passando i parametri.

Abbastanza incasinato, vero? Una volta entrati nella mentalità di Qml e c++ le cose diventano più chiare - e se le ho capite io che mi sono interessato del mondo Qt e Qml solo per svago...

Parere personale, comoda l'idea di suddividere come nel web la parte di front da quella di backend, utile per scrivere codice a più mani: uno o più programmatori possono dedicarsi allo sviluppo dell'interfaccia e altri dedicarsi al core in C++.

Be', sono ormai quasi alla fine. Ultimo giochetto che mi ha disturbato abbastanza nell'uso del QtConcurrent. Leggendo i vari tutorial online mentre ero in viaggio, tutto sembrava così banale che, mi sono detto, tutto lo avrei risolto con quindici minuti di prove. Ma quando mai. Innanzitutto prima di tirare in ballo la teoria dell'alibi e che sono gli altri che sbagliano, che il framework Qt fa schifo, il c++ è limitato o il compilatore non mi capisce, una sana autocritica mi spinge a mettere le mani avanti e dare la probabile causa del mio fallimento iniziale alla mia non approfondita conoscenza del linguaggio C++. Il tutto è nato al mio volere approfondire alcune feature dell'oggetto QtConcurrent, nel dettaglio il metodo map. Eppure l'help prometteva ampi sorrisi e tramonti indimenticabili:

The QtConcurrent::map(), QtConcurrent::mapped() and QtConcurrent::mappedReduced() functions run computations in parallel on the items in a sequence such as a QList or a QVector. QtConcurrent::map() modifies a sequence in-place, QtConcurrent::mapped() returns a new sequence containing the modified content, and QtConcurrent::mappedReduced() returns a single result.

Ottimo. Mi butto sull'esempio sottostante:

void scale(QImage &image)
{
    image = image.scaled(100, 100);
}

QList<QImage> images = ...;
QFuture<void> future = QtConcurrent::map(images, scale);

Semplice, avendo una lista di immagini possiamo fare in modo che QtConcurrent, in modo autonomo, prenda immagine e per immagine e in questo caso la scali alla dimensione 100 pixel per 100 pixel. La cosa bella di tutta questa faccenda è che, in modo del tutto autonomo, QtConcurrent, è in gradi di utilizzare tutti i core della macchina su cui gira l'applicativo - nel mio caso 4 avendo un i5 - e creando un thread per ogni core elabora tutti gli elementi della collection sfruttando la potenza della CPU al massimo della sua potenza. Ma non era questo il problema che ho incontrato nel suo utilizzo. Tutti gli esempi che trovavo in merito si basavano sull'utilizzo di una singola classe e non di funzioni in altre classi; in poche parole se la funzione scale vista nell'esempio qui sopra fosse presente in una nostra classe:

class testimage : public QThread
{
public:
void scale(QImage &image);
...
}

Il map qui sopra non funzionerà nel modo visto in nessuno dei modi successivi:

testimage ti();
QFuture<void> future = QtConcurrent::map(images, scale);
QFuture<void> future = QtConcurrent::map(images, &ti::scale);
QFuture<void> future = QtConcurrent::map(images, ti, testimage::scale);
...

Confesso che ho perso tempo cercando in rete ed ho trovato che era un problema comune. La soluzione più semplice, come ho trovato scritto, era definire il metodo come static. Sì, bella soluzione: così se devo accedere a property di istanza nella classe che mi devo inventare? Bella fregatura. Bello fare gli esempi che funzionano solo nei tutorial per fare vedere quant'è bella una tecnologia e un framework: aspetta che io rivoluziono classi e oggetti per fare un piacere al map di QtConcurrent. Alla fine non mi sono arreso: e disposto ad ammazzare una zanzara con un cannone, ecco che ho risolto importando nel progetto di base la classe Boost. Installata in C:\boost_x_xx_x, dobbiamo includere tale libreria nel progetto. Nel file nomeprogetto.pro si deve aggiungere la riga:

INCLUDEPATH += C:\boost_1_54_0

E la riga sopra funzionante diventerebbe:

testimage ti();
QFuture<void> future = QtConcurrent::map(images, boost::bind(&testimage::scale,&ti, _1)));

Risolto il problema dopo aver perso un mare di tempo, ecco il codice per l'elaborazione di una lista di oggetti con QtConcurrent e map. Iniziamo dal file principale main.cpp:

#include <QCoreApplication>


#include "concurrentthread.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    ConcurrentThread cT();
    QObject::connect(&cT, SIGNAL(finished()), &a, SLOT(quit()));
    cT.start();
    int resultValue = a.exec();
    cT.wait();

    return resultValue;
}

Se si ha necessità di spiegazione per il codice appena mostrato è meglio ricominciare la lettura di questo post, oppure andarsi a cercare un altro tutorial in rete più comprensibile di questo - non è difficile.

La collection di test è composta da un nostro oggetto creato ad hoc:

//entityaz.h
#ifndef ENTITYAZ_H
#define ENTITYAZ_H

#include <QObject>


class EntityAZ : public QObject
{
    Q_OBJECT
public:
    explicit EntityAZ(QObject *parent = 0);
    void setN1(float n1);
    void setN2(float n2);
    void calculateResult();
    float getN1();
    float getN2();
    float getResult();
private:
    float _n1,_n2,_result;
};

#endif // ENTITYAZ_H

// entityaz.cpp
#include "entityaz.h"

EntityAZ::EntityAZ(QObject *parent) :
    QObject(parent),_n1(0),_n2(0),_result(0)
{}

void EntityAZ::setN1(float n1)
{
    _n1 = n1;
}

void EntityAZ::setN2(float n2)
{
    _n2 = n2;
}

void EntityAZ::calculateResult()
{
    _result = _n1 - _n2;
}

float EntityAZ::getN1()
{
    return _n1;
}

float EntityAZ::getN2()
{
    return _n2;
}

float EntityAZ::getResult()
{
    return _result;
}

E' banale il codice: quest'oggetto raccoglie un due numeri ed esegue un banale calcolo di sottrazione.

Ora il codice principale che utilizza l'oggetto QtConcurrent:

// concurrentthread.h
#ifndef CONCURRENTTHREAD_H
#define CONCURRENTTHREAD_H

#include <QObject>

#include <QThread>

#include <QtConcurrent/QtConcurrent>

#include <QFuture>

#include <QFutureWatcher>

#include <iostream>

#include <QDebug>

#include <QList>

#include <boost/bind.hpp>

#include "entityaz.h"
#include <QMutex>

#include <QTimer>


class ConcurrentThread : public QThread
{
    Q_OBJECT
public:
    explicit ConcurrentThread(QThread *parent = 0);
    ~ConcurrentThread();
    void run();
    void spin(EntityAZ *&item);

private:
    QList<EntityAZ *> *_coll;
    QFutureWatcher<void> _futureWatcher;
    QMutex _mutex;
    const int _counter;
    const int _waitMilliseconds;

signals:

public slots:
    void showMessageTimerInterrupt();
};

#endif // CONCURRENTTHREAD_H

// concurrentthread.cpp
#include "concurrentthread.h"

ConcurrentThread::ConcurrentThread(QThread *parent) :
    _counter(100), _waitMilliseconds(300), QThread(parent)
{
    _coll = new QList<EntityAZ *>;
}

ConcurrentThread::~ConcurrentThread()
{
    while (!_coll->isEmpty())
    {
        EntityAZ *item = _coll->first();
        qDebug() << "Result "<< item->getResult();
        _coll->removeFirst();
        delete item;
    }
    delete _coll;
}

void ConcurrentThread::run()
{
    QTimer _timer; // <- this can't be instanciated in constructor (different thread)
    connect(&_timer, SIGNAL(timeout()), this, SLOT(showMessageTimerInterrupt()));
    _timer.start(1432);
    // Fill collection example
    for (int i=0;i<_counter;i++)
    {
        EntityAZ *item = new EntityAZ;
        item->setN1(i);
        item->setN2(_counter - i);
        _coll->append(item);
    }

    // Start parallel calculation
    connect(&_futureWatcher, SIGNAL(finished()), this, SLOT(quit()), Qt::DirectConnection);
    connect(&_futureWatcher, SIGNAL(finished()), &_timer, SLOT(stop()), Qt::BlockingQueuedConnection);
    _futureWatcher.setFuture(QtConcurrent::map(*_coll,boost::bind(&ConcurrentThread::spin,this, _1)));
    exec(); //start event loop, and the timer
    _futureWatcher.waitForFinished(); // <- block loop and timer
}

void ConcurrentThread::spin(EntityAZ *&item)
{
    item->calculateResult();
    this->msleep(_waitMilliseconds);
    _mutex.lock();
    qDebug() << item->getResult() << " in thread " << QThread::currentThreadId();
    _mutex.unlock();
}

void ConcurrentThread::showMessageTimerInterrupt()
{
    _mutex.lock();
    qDebug() << "******** TIMER ************";
    _mutex.unlock();
}

Quando si istanzia questa classe, viene creato la collection del nostro oggetto custom nella collection _coll. Una volta lanciato con start questo thread, viene popolata questa collection - for (int i=0;i<_counter;i++) { ... } - e siccome il tutto non è abbastanza complesso viene istanziato anche un oggetto timer per la visualizzazione di un messaggio ogni 1432 millisecondi. Potrebbe sembrare completamente inutile questa aggiunta, invece può essere molto utile per capire che il nostro applicativo è stato sviluppato come si deve; come detto fin dall'inizio di questo post, una necessità pressoché obbligatoria in un applicativo che utilizza il framework Qt, è che l'event loop dei thread possano girare sempre senza problemi in modo che possono gestire eventuali eventi.

Utilizzando un QFuture possiamo collegare gli eventi per comunicare con gli altri thread:

    connect(&_futureWatcher, SIGNAL(finished()), this, SLOT(quit()), Qt::DirectConnection);
    connect(&_futureWatcher, SIGNAL(finished()), &_timer, SLOT(stop()), Qt::BlockingQueuedConnection);

Utilizzando il signal finished, quando QtConcurrent finirà l'elaborazione della collection, invierà il segnale agli oggetti collegati, in questo caso all'event loop del thread attuale e al QTimer per bloccare la visualizzazione dei messaggi. Ma cosa si vuole dire con bloccare l'event loop del thread attuale? Dopo il codice appena visto, infatti, si ha questo codice:

_futureWatcher.setFuture(QtConcurrent::map(*_coll,boost::bind(&ConcurrentThread::spin,this, _1)));
    exec(); //start event loop, and the timer

La prima riga fa avviare effettivamente l'elaborazione, mentre la seconda, come ormai si saprà, avvia l'event loop del thread attuale che rimarrà in attesa di eventi come il QTimer prima istanziato o l'evento "quit".

La funzione chiamata per l'elaborazione della coda è la seguente:

void ConcurrentThread::spin(EntityAZ *&item)
{
    item->calculateResult();
    this->msleep(_waitMilliseconds);
    _mutex.lock();
    qDebug() << item->getResult() << " in thread " << QThread::currentThreadId();
    _mutex.unlock();
}

Essa viene chiamata per ogni singolo item della collection. Per ognuno di esso viene eseguita la funzione "calculateResult" e si attende un tempo fittizio per simulare una lenta elaborazione, quindi viene visualizzato un messaggio a video. _mutex (instanziato con QMutex _mutex) è paragonabile al Monitor.Enter del mondo Framework .net, per il blocco esclusivo di un blocco di codice.

La visualizzazione finale a video è qualcosa di questo tipo:

esempio 4

Ho detto che era un quad core la mia macchina? Infatti si può vedere dal nome del thread che sono esattamente quattro i thread paralleli che elaborano la coda (è sufficiente esaminare il codice esadecimale univoco nell'immagine precedente).

Ultimo volo pindarico dedicato al distruttore di classe in cui viene eseguito questo codice per la distruzione corretta degli oggetti:

    while (!_coll->isEmpty())
    {
        EntityAZ *item = _coll->first();
        qDebug() << "Result "<< item->getResult();
        _coll->removeFirst();
        delete item;
    }
    delete _coll;

In modo molto banale il ciclo while richiama ogni singolo elemento della collection e con il delete distruggiamo effettivamente l'oggetto liberando le risorse occupate. Chi abitualmente si guadagna la pagnotta con il framework .net o il java, sa che questo codice è diventato pressoché inutile grazie al garbage collector che penserà lui al nostro posto a distruggere gli oggetti e liberare risorse. E siccome il mondo Qt non si vuole fare mancare niente per rendere la vita del programmatore più agevole, si è inventato più di un modo per evitare il problema della non distruzione degli oggetti.

Il metodo più semplice: alla creazione di ogni oggetto con new, tra i parametri del costruttore inserire il nome di un oggetto Qt creato precedentemente. Ed ecco l'utilità dell'oggetto QcoreApplication o QApplication:

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

QOtherObject *oo=new QOtherObject(&a);
QAnotherObject *ao=new QAnotherObject(12,50,&a);
return a.exec();
}
...
class QOtherObject : public QObject
{
public:
QOtherObject(QObject *parent = 0) : Qobject(parent)
{
QAnotherObject *qa2=new QAnotherObject(this);
...
}
...    
};

Quando l'oggetto “a”, QcoreApplication, sarà distrutto alla fine dell'operazione, anche tutti gli oggetti sottostanti aventi lui o qualche altro oggetto derivato da lui, saranno distrutti.

Inoltre Qt propone anche degli oggetti appositi per la gestione dei puntatori: QPointer, QsharedPointer, QscopePointer. Prendiamo da esempio solo l'ultimo:

{
QScopedPointer<myQtClass> myc=new myQtClass;
...
}

Con questa sintassi siamo sicuri che l'oggetto myc sarà distrutto alla fine dello scope di questo codice. L'utilità non si ferma di certo qui perché oltre a questo compito questi oggetti ci consentono anche di verificare che l'oggetto sia ancora attivo e che altro codice non l'abbia distrutto:

if (myc.isNull()) ...

QSharedPointer permette anche di agganciare eventi alla fase di distruzione e così via.

Ma alla fine della fiera sono utili queste due ultime tecniche? Sinceramente le conosco solo e provate per piccoli esempi, ma non me ne sono mai fatte niente preferendo la vecchia e caro costruttore e distruttore – almeno per quel che serve a me.

Ok, basta.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese