Skip to content

Classes and widgets to support asynchronous calculations

License

Notifications You must be signed in to change notification settings

lexxmark/qt-async

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This library has some usefull Qt classes and widgets for asyncronious operations.

qt-async demo video

Brief overview

The library introduces async values. Like std::future async value holds the result of async operation or error (if operation was unsuccessfull). Also it holds progress if async operation is still executing and has stateChanged signal that fires when async value is switching between progress, result and error.

Like future<T> std::async(Callable fn) qt-async library has a set of functions bool asyncValueRunXXX(AsyncValue& value, Callable fn, ProgressArgs... args) to make asyncronious calculation for the value. All these functions switch value to a progress state (and pass args to progress constructor) and invoke fn(progress, value) so fn implementation can use progress to report executing progress or ask if execution was requested to stop. value parameter is used to set a result of the operation or an error if calculation has failed.

To represent such async values in GUI qt-async library has async widgets. Every time async value state changes, async widget creates sub-widget for that state (ProgressWidget, ErrorWidget or ValueWidget). Similar to async value where user should supply 'fn' that actually calculates value, user should supply to async widget a factory how to create ValueWidget (it can be overriden virtual function or std::function).

Basic use

Declare async value object that holds values of type QString and initialized with string "Hello World!":

using AsyncQString = AsyncValue<QString>;
AsyncQString value(AsyncInitByValue{}, "Hello World!");

When you need to calculate value, call one of the asyncValueRunXXX functions:

    bool success = asyncValueRunThreadPool(value, [](AsyncProgress& progress, AsyncQString& value) {

        // a long calculations
        for (auto i : {0, 1, 2, 3, 4})
        {
            // report progress i/5
            progress.setProgress(i, 5);
			
            // check if calculation was stopped
            if (progress.isStopRequested())
            {
                // report error
                value.emplaceError("Stopped");
                return;
            }

            QThread::sleep(1);
        }

        // do final processing
        progress.setProgress(1.f);
        progress.setMessage("Processing...");
        QThread::sleep(1);

        QString val = "Loaded value is 42";
        // set calculated value
        value.emplaceValue(std::move(val));

    }, "Loading...", ASYNC_CAN_REQUEST_STOP::YES);

The available functions are:

Somewhere in GUI code declare async widget:

        // create widget
        auto valueWidget = new AsyncWidgetFn<AsyncQString>(parent);
        
        // set callback that creates sub-widget to show QString value
        valueWidget->createValueWidget = [](QString& value, QWidget* parent) {
            // create QLabel
            return AsyncWidgetProxy::createLabel(value, parent);
        };

        // assign async value with widget
        valueWidget->setValue(&value);

Instead of callbacks you can derive widget class from AsyncWidgetBase or AsyncWidget classes and override 4 functions:

    // creates widget to show value
    virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent);
    
    // creates widget to show error
    virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent);
    
    // creates widget to show progress
    virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent);
    
    // creates widget when no async value has been assigned to async widget
    virtual QWidget* createNoAsyncValueWidgetImpl(QWidget* parent);

AsyncValue API

Most of the AsyncValue functions can be found in AsyncValueTemplate<...> base class.

User can initialize AsyncValue using value or error, AsyncInitByValue and AsyncInitByError tag classes are used to distinguish these two cases:

    using AsyncInt = AsyncValue<int>;
    
    // init by value
    AsyncInt value(AsyncInitByValue{}, 12);
    // init by parent and value
    auto value = new AsyncInt(parent, AsyncInitByValue{}, 12);
    // init by error
    AsyncInt value(AsyncInitByError{}, "No int available");
    // init by parent and error
    auto value = new AsyncInt(parent, AsyncInitByError{}, "No int available");

There is stateChanged signal that is emitted when async value's state changes between value, error and progress. NOTE: you cannot modify async value in it's stateChanged signal handler otherwise deadlock will happen.

    QObject::connect(value, &AsyncValueBase::stateChanged, [](ASYNC_VALUE_STATE state) {
        // async value state change handler
    });

To get the content of the async value use access functions and supply callables to access either value or error or progress:

    // get any content of the async value
    value.access([](int value) { /* access int value here */ },
                 [](AsyncError& error) { /* access error here */ },
                 [](AsyncProgress& progress) { /* access progress here */ });
                 
   // get value of the async value
   bool success = value.accessValue([](int value) { /* access int value here */ });

User can assign a value using the following functions:

    AsyncValue<std::string> value(...);
    
    // create value by passing value's constructor parameters
    value.emplaceValue(5, 'b');
	
    // or create value and pass it to async value
    auto str = std::make_unique<std::string>(5, 'b');
    value.moveValue(std::move(str));

User can assign an error in a similar way:

    AsyncValue<std::string> value(...);
    
    // create error by passing error's constructor parameters
    value.emplaceError("Some error hapenned");
	
    // or create error and pass it to async value
    auto err = std::make_unique<AsyncError>("Some error hapenned");
    value.moveError(std::move(err));

startProgress and completeProgress functions are used by asyncValueRunXXX functions to start and finish progress:

    template <typename AsyncValueType, typename Func, typename... ProgressArgs>
    bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs)
    {
        // create progress
        auto progress = std::make_unique<typename AsyncValueType::ProgressType>(std::forward<ProgressArgs>(progressArgs)...);
        auto progressPtr = progress.get();
        
        // try to switch async value to progress state
        if (!value.startProgress(std::move(progress)))
            return false;

        QtConcurrent::run(pool, [&value, progressPtr, func = std::forward<Func>(func)](){
            SCOPE_EXIT {
                // finish progress
                value.completeProgress(progressPtr);
            };

            // run calculation
            func(*progressPtr, value);
        });

        return true;
    }

Also user has an ability to wait async value for result:

    AsyncValue<int> value(...);
    ...
    value.wait([](int value) { /* access int value here */ },
               [](AsyncError& error) { /* access error here */ });

Runnable values

Usually it's more convinient to hide details how value is calculated.

AsyncValueRunableAbstract and AsyncValueRunableFn classes are used in this case.

    using AsyncQPixmap = AsyncValueRunableFn<QPixmap>;
    AsyncQPixmap value(AsyncInitByError{}, "Select image file path.");
    
    // set callback to start async value calculation
    value.deferFn = [&value](const AsyncQPixmap::RunFnType& fn) {
        asyncValueRunThread(value, fn, "Loading image...", ASYNC_CAN_REQUEST_STOP::NO);
    };
    // set callback to calculate actual value
    value.runFn = [](AsyncProgressRerun& progress, AsyncQPixmap& value) {

        auto url = getImageUrl();
        QImage image(url);

        for (auto i : {0, 1, 2, 3})
        {
            if (progress.isRerunRequested())
            {
                // exit and retry load image with new url
                return ;
            }

            progress.setProgress(i, 4);

            // do some heavy work
            QThread::sleep(1);
        }

        if (image.isNull())
            value.emplaceError(QString("Cannot load image from file '%1'.").arg(url));
        else
            value.emplaceValue(QPixmap::fromImage(image));
    };

To calculate value user need to call run() method:

    value.run();

This does more than just calls value calculation in async manner. If previous calculation is not completed yet it tries to stop and rerun calculation. It doesn't block calling thread. Here is a code of the run function:

    void run()
    {
        bool isInProgress = accessProgress([](ProgressType& progress) {
            // if we are in progress already -> just request rerun
            progress.requestRerun();
        });

        if (isInProgress)
            return;

        // run later
        deferFn([this] (ProgressType& progress, ThisType& value) {

            for (;;)
            {
                // try to calculate value
                runFn(progress, value);
                // if no rerun was requested -> we are good to exit
                if (!progress.resetIfRerunRequested())
                    break;
            }

        });
    }

The code is quite straightforward.

User can use the same widgets to show runnable values in GUI:

        auto valueWidget = new AsyncWidgetFn<AsyncQPixmap>(ui->widget);

        valueWidget->createValueWidget = [](QPixmap& value, QWidget* parent) {
            auto label = new QLabel(parent);
            label->setAlignment(Qt::AlignCenter);
            label->setPixmap(value);
            label->setStyleSheet("border: 1px solid black");
            return label;
        };

        valueWidget->setValue(&value);

Advanced example

In the MyPixmap.h file you can find a complete example how to adopt async values and widgets for your needs.

Let's say you have some class to load and store QPixmap. Loading image from external url should be done asynchronously. Here we just inherit our class from AsyncValueRunableAbstract<QPixmap> class and override deferImpl and runImpl functions. When image url has changed we call run to perform image loading in a separate thread:

class MyPixmap : public AsyncValueRunableAbstract<QPixmap>
{
public:
    // init pixmap with an error
    MyPixmap()
        : AsyncValueRunableAbstract<QPixmap>(AsyncInitByError{}, "Select image file path.")
    {
    }

    // returns umage url
    // threadsafe
    QString imageUrl() const
    {
        QReadLocker locker(&m_urlLock);
        return m_imageUrl;
    }

    // sets new image url and requests image loading
    // threadsafe
    void setImageUrl(QString url)
    {
        // change image url
        {
            QWriteLocker locker(&m_urlLock);
            m_imageUrl = url;
        }

        // reload image
        run();
    }

protected:
    // hide run from public
    using AsyncValueRunableAbstract<QPixmap>::run;

    // actual image loading will be performed in a separate thread
    void deferImpl(RunFnType&& func) final
    {
        asyncValueRunThread(*this, func, "Loading image...", ASYNC_CAN_REQUEST_STOP::NO);
    }

    // image loading code
    void runImpl(ProgressType& progress) final
    {
        auto url = imageUrl();

        QImage image(url);

        for (auto i : {0, 1, 2, 3})
        {
            if (progress.isRerunRequested())
            {
                // exit and retry load image with a new path
                return ;
            }

            progress.setProgress(i, 4);

            // do some heavy work
            QThread::sleep(1);
        }

        if (image.isNull())
            emplaceError(QString("Cannot load image from file '%1'.").arg(url));
        else
            emplaceValue(QPixmap::fromImage(image));
    }

private:
    mutable QReadWriteLock m_urlLock;
    QString m_imageUrl;
};

The widget for MyPixmap class could be implemented like this:

class MyPixmapWidget : public AsyncWidget<MyPixmap>
{
public:
    MyPixmapWidget(QWidget* parent, MyPixmap* value)
        : AsyncWidget<MyPixmap>(parent)
    {
        setValue(value);
    }

protected:
    // creates QLabel to show QPixmap image
    QWidget* createValueWidgetImpl(QPixmap& value, QWidget* parent) final
    {
        auto label = new QLabel(parent);
        label->setAlignment(Qt::AlignCenter);
        label->setPixmap(value);
        label->setStyleSheet("border: 1px solid black");
        return label;
    }
};

In Demo application we have 2nd tab that shows GUI for MyPixmap instance. Once user changes image url using button or editbox, the MyPixmapWidget will change its content to progress widget and show error or image on loading completion.

Customizations

All async value classes are inherited from AsyncValueTemplate template class:

template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t, typename TrackErrorsPolicy_t>
class AsyncValueTemplate : public AsyncValueBase
{
    ...
};

So users can override all type parameters to better adopt async values to different environments.

By default ErrorType_t parameter is an AsyncError class:

class AsyncError
{
public:
    AsyncError(QString text)
        : m_text(std::move(text))
    {}

    QString text() const { return m_text; }

private:
    QString m_text;
};

The ProgressType_t parameter represented by AsyncProgress with the following functions:

    // the text describing the current progress
    QString message() const;
    // current progress position in [0, 1]
    float progress() const;
    // returns true if progress can handle stop requests
    bool canRequestStop() const;
    // returns true if progress was requested to stop
    bool isStopRequested() const;

    // sets text describing the current progress
    void setMessage(QString message);
    // sets current progress position in [0, 1]
    void setProgress(float progress);
    // sets current position using current step and total number of steps
    template <typename Num>
    void setProgress(Num current, Num total)
    {
        if (total != 0)
            setProgress(static_cast<float>(current) / static_cast<float>(total));
    }
    // requests stop of the current progress
    void requestStop();

TrackErrorsPolicy_t parameter is used to customize reaction to inconsistent or incorrect situations. By default AsyncTrackErrorsPolicyDefault class is used:

 struct AsyncTrackErrorsPolicy
{
    // class to catch emit deadlocks
    struct EmitGuard
    {
        EmitGuard(AsyncTrackErrorsPolicyNone&) {}
    };

    using EmitGuardType = EmitGuard;

    // called when async value is changing inside stateChanged signal handler
    // this incorrect situation will lead to a deadlock 
    void trackEmitDeadlock() const;
    
    // called when async value is in progress and destructing at the same time
    // usually user should call value.stopAndWait(); before value destruction
    void inProgressWhileDestruct() const;
    
    // called if startProgress is called while value is in progress already
    void startProgressWhileInProgress() const;
    
    // called when completeProgress is called with different progress object
    void tryCompleteAlienProgress() const;
    
    // called inside value.completeProgress function if niether value nor error was assigned to async value
    void incompleteProgress() const;
};

To use async values with different asynchronious API or frameworks you can create asynValueRunXXX like function. The schema is simple:

template <typename AsyncValueType, ...>
bool asyncValueRunMyFramework(AsyncValueType& value, Routine func, ...)
{
    // create progress
    auto progress = std::make_unique<typename AsyncValueType::ProgressType>(...);
    auto progressPtr = progress.get();
    
    // try to switch value to progress state
    if (!value.startProgress(std::move(progress)))
        return false;

    // move async value and calculation routine to MyFramework
    MyFramework::AsyncCall([&value, progressPtr, func = std::forward<Func>(func)]() {
        // invoke routine
        func(...);
        
        // finalize progress
        value.completeProgress(progressPtr);
    });
}

About

Classes and widgets to support asynchronous calculations

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published