3
\$\begingroup\$

I'm writing a GUI for different VME modules (electronics). There are several of them which are used for the data acquisition. For example, an ADC produces the digitized maximum voltage on its input. So I need a way to visualize this data. Almost always this way is a histogram. So I've chosen the Qwt library. The straightforward solution would be to connect the QTimer's timeout signal to a slot in which the data is read and then to update a histogram. But it's bad, because no one knows in advance how long the reading and the processing would take, and most likely it would block the main loop. So, multi-threading.

The core logic was stolen from this Qt's example.

I think this code would be useful for people with the similar needs.


P.S. Sorry for the long post :)


enter image description here

We have two classes in this example:

  1. DataGeneratorThread: is a QThread and is responsible for the actual data generation (generates random numbers according to a gaussian p.d.f. )
  2. RealTimeHistogram: the main widget, a user interface

DataGeneratorThread class

DataGeneratorThread.h

#pragma once

#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QMetaType>

#include "qwt_samples.h"

Q_DECLARE_METATYPE(QVector<QwtIntervalSample>)

class DataGeneratorThread : public QThread
{
    Q_OBJECT

    public :
        DataGeneratorThread( QObject *parent = nullptr );
        ~DataGeneratorThread() override;

    public slots :
        // These slots are the bridge between this thread
        // and the main one: they are to be called from the
        // parent thread
        void OnStart( size_t size );
        void OnStop();
        void OnPause();
        void OnResume();
        void Dump();

    signals :
        // This signal is how this thread returns data
        // it is connected to the slot in the parent thread
        void DataGenerated( const QVector<QwtIntervalSample>& data, unsigned nEvents );

    protected :
        // The main function
        void run() override;

    private :
        // Flags to manipulate the control flow in
        // the run() function
        bool fAbort = false;
        bool fPause = false;
        bool fRestart = false;
        bool fDumpRequest = false;

        // Mutex to protect data access
        QMutex fMutex;
        // Condition to be able to put this thread
        // in the sleeping state when needed
        QWaitCondition fCondition;

        // Parameter of data generation
        size_t fSize;
};

DataGenerator.cpp

#include <algorithm>
#include <random>

#include <QMutexLocker>

#include "DataGenerator.h"


DataGeneratorThread::DataGeneratorThread( QObject* parent ) :
    QThread( parent )
{
    qRegisterMetaType<QVector<QwtIntervalSample>>();
}

DataGeneratorThread::~DataGeneratorThread()
{
    fMutex.lock();
        fAbort = true;// Signal to return from run() (see below)
        fCondition.wakeOne();// Might be sleeping so awake
    fMutex.unlock();

    wait();// Wait for return from run() before QThread::~QThread()
}

The communication between this thread and the main one are performed through 5 public slots (they are to be called on the main thread's side) :

void DataGeneratorThread::OnStart( size_t size )
{
    QMutexLocker locker( &fMutex );

    fAbort = false;
    fPause = false;

    fSize = size;

    if( !isRunning() )
    {
        start( LowPriority );
    }
    else
    {
        fRestart = true;// Signal to restart calculation (see below)
        fCondition.wakeOne();//Might be sleeping so awake
    }
}

This function is called when the main thread requests the new data to be generated.

void DataGeneratorThread::OnStop()
{
    QMutexLocker locker( &fMutex );
    fAbort = true;// Signal to return from run (see below)
    fCondition.wakeOne();
}

This function is called when the main thread want to stop this thread.

void DataGeneratorThread::OnPause()
{
    QMutexLocker locker( &fMutex );
    fPause = true;// 
    fCondition.wakeOne();
}

This function is called when the main thread wants to temporarily stop data generation. This function puts this thread into sleep (not directly though, see the run() function).

void DataGeneratorThread::OnResume()
{
    QMutexLocker locker( &fMutex );
    fPause = false;
    fCondition.wakeOne();
}

This function is complementary to the OnPause().

void DataGeneratorThread::Dump()
{
    QMutexLocker locker( &fMutex );
    fDumpRequest = true;
}

This function signals that the main thread want to read the generated data.

As you might notice, there is the fCondition.wakeOne() line in all but the last slots. This line wakes this thread if it's sleeping (see the run() function below). We don't awake the thread in the Dump() function intentionally, so we sure that there is no data update (no DataGenerated signal is emitted) when the thread is sleeping.

void DataGeneratorThread::run()
{
    forever
    {
        fMutex.lock();
            const size_t size = fSize;
        fMutex.unlock();

        QVector<QwtIntervalSample> data( size );
        unsigned n = 0;
        // Initialize histogram
        for( size_t i = 0; i < size; ++i )
        {
            data[i] = QwtIntervalSample( 0.0, i, i + 1 );
        }

        std::random_device rd;
        std::mt19937 gen( rd() );

        forever
        {
            if( fAbort )// Accessible from the OnStart() and OnStop()
            {
                return;
            }
            if( fDumpRequest )// Accessible from the Dump()
            {
                emit DataGenerated( data, n );// This signal is catched by the RealTimeHistogram
                fMutex.lock();
                    fDumpRequest = false;
                fMutex.unlock();
            }
            if( fPause )
            {
                fMutex.lock();
                    fCondition.wait( &fMutex );// Go to sleep
                fMutex.unlock();
            }
            if( fRestart )
            {
                break;
            }
            std::normal_distribution<> g( size / 2., size / 20. );
            size_t rand = std::round( g( gen ) );
            if( rand < size )
            {
                data[ rand ].value += 1;// Fill histogram
                n++;
            }
        }
        // The only way to break from the innermost forever loop is the fRestart flag
        fMutex.lock();
            fRestart = false;
        fMutex.unlock();
    }
}

As you can see the run() function consists of an infinite loop. The only way to return from it is the fAbort flag. The meaning of other flags is obvious: fPause puts this thread into sleep; fRestart restarts data generation; fDumpRequest signals that the data must be sent to the main thread.

RealTimeHistogram class

This class contains the user interface and is supposed to be instantiated in the main thread. It is shown on the gif in the beginning of the post.

RealTimeHistogram.h

#pragma once

#include <QWidget>
#include "DataGenerator.h"

class QTimer;
class QPushButton;

class QwtPlot;
class QwtPlotHistogram;
class QwtPlotMarker;


class RealTimeHistogram : public QWidget
{
    Q_OBJECT

    private :
        static constexpr size_t N_BINS = 4096;

        QTimer      *fTimer;
        QPushButton *fStartButton, *fStopButton, *fPauseButton;

        QwtPlot             *fPlot;
        QwtPlotMarker       *fStatistics;
        QwtPlotHistogram    *fHisto;

        DataGeneratorThread fThread;

    private :
        void CreateWidgets();
        void CreateTimer();

    public slots :
        void InitHistogram();
        void UpdateData( const QVector<QwtIntervalSample>& data, unsigned nEvents );
        void UpdateStat( unsigned nEvents );
        void StartTimer();
        void StopTimer();
        void Pause();

    public :
        RealTimeHistogram( QWidget* parent = nullptr );
        ~RealTimeHistogram() override = default;
};

RealTimeHistogram.cpp

#include <QPushButton>
#include <QTimer>
#include <QFrame>
#include <QVBoxLayout>
#include <QHBoxLayout>

#include <qwt_plot.h>
#include <qwt_plot_histogram.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_layout.h>
#include <qwt_legend.h>
#include <qwt_plot_marker.h>

#include "RealTimeHistogram.h"


RealTimeHistogram::RealTimeHistogram( QWidget* parent ) :
    QWidget( parent )
{
    CreateWidgets();
    CreateTimer();

    connect( &fThread, &DataGeneratorThread::DataGenerated, this, &RealTimeHistogram::UpdateData );
}

This is how the data goes from the DataGeneratorThread: this class "catches" the signal with the data provided as arguments.

void RealTimeHistogram::CreateWidgets()
{
    QVBoxLayout* vLayout = new QVBoxLayout();

    // Buttons
    fStartButton = new QPushButton( "START" );
        fStartButton->setStyleSheet( "background-color: #a8ffe9" );
        connect( fStartButton, &QPushButton::clicked, this, &RealTimeHistogram::InitHistogram );
        connect( fStartButton, &QPushButton::clicked, this, &RealTimeHistogram::StartTimer );
    fPauseButton = new QPushButton( "PAUSE" );
        fPauseButton->setStyleSheet( "background-color: #b6b6b6" );
        connect( fPauseButton, &QPushButton::clicked, this, &RealTimeHistogram::Pause );
    fStopButton = new QPushButton( "STOP" );
        fStopButton->setStyleSheet( "background-color: #6d6d6d; color: #ffffff" );
        connect( fStopButton, &QPushButton::clicked, &fThread, &DataGeneratorThread::OnStop );
        connect( fStopButton, &QPushButton::clicked, this, &RealTimeHistogram::StopTimer );

    QFrame *buttonFrame = new QFrame();
    QHBoxLayout *buttonLayout = new QHBoxLayout();

    buttonLayout->addWidget( fStartButton );
    buttonLayout->addWidget( fPauseButton );
    buttonLayout->addWidget( fStopButton );

    buttonFrame->setLayout( buttonLayout );

    vLayout->addWidget( buttonFrame );

    // Histogram
    fPlot = new QwtPlot();
        fPlot->setAxisTitle( QwtPlot::yLeft, "Number of events" );
        fPlot->setAxisTitle( QwtPlot::xBottom, "Value" );
        fPlot->setCanvasBackground( QColor( "#b6b6b6" ) );
        fPlot->plotLayout()->setAlignCanvasToScales( true );
        fPlot->insertLegend( new QwtLegend(), QwtPlot::RightLegend );

    QwtPlotGrid *grid = new QwtPlotGrid;
        grid->enableX( true );
        grid->enableY( true );
        grid->attach( fPlot );
        grid->setMajorPen( QPen( QColor( "#ffffff" ), 0, Qt::DotLine ) );

    fHisto = new QwtPlotHistogram( "Data" );
        fHisto->setPen( QPen( QColor( "#a8ffe9" ) ) );
        fHisto->setBrush( QBrush( QColor( "#a8ffe9" ) ) );
    fHisto->attach( fPlot );

    fStatistics = new QwtPlotMarker();
    fStatistics->attach( fPlot );

    fPlot->show();
    fPlot->replot();

    UpdateStat( 0 );

    vLayout->addWidget( fPlot );

    setLayout( vLayout );
}

This function creates widgets which is irrelevant to the main topic. Although it is worth to note that those slots in the DataGeneratorThread are connected to the clicked signals of the buttons.

void RealTimeHistogram::CreateTimer()
{
    fTimer = new QTimer( this );
        fTimer->setInterval( 1000 );
    connect( fTimer, &QTimer::timeout, &fThread, &DataGeneratorThread::Dump );
}

That's it : the timer just generates a dump request every second.

void RealTimeHistogram::InitHistogram()
{
    QVector<QwtIntervalSample> histData;

    for( size_t i = 0; i < N_BINS; ++i )
    {
        histData.push_back( QwtIntervalSample( 0.0, i, i + 1 ) );
    }

    fHisto->setSamples( histData );
    fThread.OnStart( N_BINS );
}

Nothing special here (and maybe even redundant), except the last line which starts (or restarts) data generation.

void RealTimeHistogram::StartTimer()
{
    if( not fTimer->isActive() )
    {
        fTimer->start();
    }
}

void RealTimeHistogram::StopTimer()
{
    if( fTimer->isActive() )
    {
        fTimer->stop();
    }
}

This is how we manage the timer.

void RealTimeHistogram::Pause()
{
    QPushButton* button = qobject_cast<QPushButton*>( this->sender() );
    if( button->text() == "PAUSE" )
    {
        fThread.OnPause();
        button->setText( "RESUME" );
    }
    else if( button->text() == "RESUME" )
    {
        fThread.OnResume();
        button->setText( "PAUSE" );
    }
}

Depending on the name of the button this function either pauses or resumes data generation.

void RealTimeHistogram::UpdateData( const QVector<QwtIntervalSample>& data, unsigned nEvents )
{
    fHisto->setSamples( data );
    fPlot->replot();
    UpdateStat( nEvents );
}

Here is how we use the generated data. Remember this slot is connected to the DataGeneratorThread::DataGenerated() signal.

void RealTimeHistogram::UpdateStat( unsigned nEvents )
{
    QwtScaleMap xMap = fPlot->canvasMap( QwtPlot::xBottom );
        double x = (xMap.s1() + xMap.s2()) * 0.5;
    QwtScaleMap yMap = fPlot->canvasMap( QwtPlot::yLeft );
        double y = (yMap.s1() + yMap.s2()) * 0.3;
    fStatistics->setValue( x, y );
    fStatistics->setLabel( QwtText( QString( "Number of events = %1" ).arg( nEvents ) ) );
}

The function to place the label with the number of events correctly on the canvas.


EDIT

Full code can be found at this repo.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Is there any repo I could clone and play with? \$\endgroup\$ Nov 5 at 7:05
  • \$\begingroup\$ @Incomputable, Added a link to the repo. And any feedback would be appreciated. \$\endgroup\$
    – LRDPRDX
    Nov 5 at 12:05
1
\$\begingroup\$

Build

I believe build scripts are a major part of the code. If nobody can incorporate it into their build, then the piece code is useless. Yours is good, but it seems the default distribution provides pkg-config files. I tried to use pkg_check_modules inside cmake, but after scratching my head for two hours I gave up on doing it by the book, for now (I will try to write a find module and link a github gist here, sometime later).

Bug

When I press start, then pause (pause button changes into resume), then start, the resume button does not change into pause as it should.

## Memory leak?

I'm not proficient in Qt's memory management style, but from what I know, all QObject instances must specify a parent if they want to be hooked into automatic memory management system. Constructor inside of the histogram does not specify parent for member objects and the destructor is trivial, which seems wrong.

Synchronization

The only reason the code works as is is because of X86 quirks of atomic read/write by default. On a platform where that is not the case, volatile is required for them to work.

Instead of fine grained locking, it might be better to use atomic on the combination of state variable (combine start, pause and stop into one enum-like atomic variable). There is also potential deadlock in the destructor: if the worker thread will check abort, then user pauses and quickly exits (well, human probably cannot do that), it might be possible that worker thread will see false abort and go into a pause section, where it will miss the awake on condition variable, thus stuck forever. The solution is to coalesce the state variables (start, pause, stop, dump) into one variable and read under lock, or lock all of the if branches (which is bad). The easiest solution is to use a semaphore: there is no awake signal to miss. If the condition variables will be merged into one, atomic type might be useful, since worker thread only reads from them.

\$\endgroup\$
9
  • \$\begingroup\$ This will be a bit of a rolling review. I'll keep adding to it when I'll have the time. \$\endgroup\$ Nov 5 at 16:30
  • \$\begingroup\$ Thank you for your answer. First of all, the Bug section. I intentionally left this behavior as I wanted to check everything worked as intended. And it did. \$\endgroup\$
    – LRDPRDX
    Nov 5 at 16:58
  • \$\begingroup\$ Now the Memory leak section. "...all QObject instances must specify a parent if they want to be hooked into automatic memory management system". No, they shouldn't --- it would be a nightmare. The layout and plot are responsible for the deletion of those objects. \$\endgroup\$
    – LRDPRDX
    Nov 5 at 17:02
  • \$\begingroup\$ The Build. I tried to provide a "code only" solution to the problem I encountered because there are not many (I didn't find any :) ) examples of this problem on the Internet. It's just source code. No more. No less. \$\endgroup\$
    – LRDPRDX
    Nov 5 at 17:08
  • \$\begingroup\$ Take your time. I appreciate your feedback. \$\endgroup\$
    – LRDPRDX
    Nov 5 at 17:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.