Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / QT

Responsive GUI Application Tricks

3.90/5 (11 votes)
29 Jul 2016CPOL3 min read 19.5K   173  
How to avoid blocking long operations in GUI applications, that lead to hung state.

Introduction

Often it could be better to implement long-time operations inside coroutines, which are processed as idle tasks. The support for this solution is inside the downloadable code.

Background

Blocking long-time operations (for example advanced data parsing) under GUI applications are annoying, They lead to frozen windows and other GUI elements. The idea of this code is to ensure non-blocking long time operations in the same thread, without taking care of the multithreading issues.

For example in MFC framework there is an overridable OnIdle method. In this solution it could be overridden using interface Tasks::IIdleTask from the associated source.


ExampleApp.h:

C++
// ...
//=============================================================================
class ExampleApp: public CWinApp
{
public:
   BOOL OnIdle( LONG _counter ) override;
   // ...
   void ScheduleIdleTask( boost::shared_ptr< Tasks::IIdleTask > const & _task );
   // ...
private:
   std::queue< boost::shared_ptr< Tasks::IIdleTask > > m_idleTaskQueue;
   // ... 
};

ExampleApp.cpp:

C++
//------------------------------------------------------------------------------------
BOOL ExampleApp::OnIdle( LONG _counter )
{
    BOOL moreFrameworkTasks = CWinApp::OnIdle( _counter );  // default implementation

    if( !m_idleTaskQueue.empty() )
    {
        boost::shared_ptr< Tasks::IIdleTask > task = m_idleTaskQueue.front();
        task->run();
        if( task->isDone() )
           m_idleTaskQueue.pop();
    }
    return moreFrameworkTasks || !m_idleTaskQueue.empty();
} 

//------------------------------------------------------------------------------------
void ExampleApp::ScheduleIdleTask( boost::shared_ptr< Tasks::IIdleTask > const & _task )
{
    m_idleTaskQueue.push( _task )
}

In general, the class implementing interface Tasks::IIdleTask should be derived, the run method should be overridden. The isDone method should tell, if the operation is complete.

But the idea goes one step further. The class Tasks::IdleCoroutineTask (implementing interface Tasks::IIdleTask) can be used, to schedule a yieldable coroutine as an idle task.  In this case an appropriate function could do any long-time operation "between the lines" while simultaneous GUI message handling. But the scheduled function is responsible for appropriately frequent calling the yield method of interface Tasks::IYield, that is passed as parameter. Otherwise, the GUI behaviour could be perturbed, or at least influent.

The Tasks::IYield::yield method could be invoked in two modes:

  • heueristic - the yield will occur only if the coroutine used at least 200ms of CPU, so it is better, to call too often, than to rarely.
  • unconditional - the yield will always occur

Regardless of idle time processing, coroutines are a useful utility in other issues, for example intelligent iterator. To use a coroutine explicitely, the Tasks::Coroutine class should be used, it is instantialed only by static create method. It does not automatically pass the control to coroutine, the switchTo method should be used instead.

In Windows, coroutines are based on fiber system mechanism. Since, each fiber uses its own stack, frequent creation of fibers could affect the system performance. So the pool of fibers is implemented; insted of creating and deleting fibers each time, the ready to use fibers, are kept all the time. When a coroutine is created, a fiber is taken from the pool and associated, later is recycled when coroutine is done. 

Using the code

Prerequisities:

  1. The MFC application should be created (or Qt with possibility to adjust the code).
  2. The downloaded code should be included into the project
  3. The application class should be modified in similar manner to ExampleApp

Later it could be done in the following way:

C++
#include "coroutine_function.hpp"
#include <boost/shared_ptr.hpp>

//
//-----------------------------------------------------------
void longOperarion( Tasks::IYieldRef const & _refYield )
{
    bool done = false;

    while( !done )
    {
        // ... any actions

        _refYield.get().yield();  // By default heueristic mode
                                  // in fact standard message handling will be done

        // Suppose, getNextFileName returns the next file to be processed
        std::ifstream file( getNextFileName(), std::ifstream::in );  

        while( !file.eof() )
        {
            std::string line;
            std::getline( file, line );

            // ... any actions

            _refYield.get().yield(); // Redundant yields are reduced due to heueristic
        }
    }
} 

// ... any code

//----------------------------------------------------------- 
void ExampleDlg::OnDoOperation()
{
    boost::shared_ptr< Tasks::IdleCoroutineTask > idleTask( 
            new Tasks::IdleCoroutineTask( &longOperarion )
        );

    theApp.ScheduleIdleTask( idleTask );
}

 

Moreover, the scheduled function can contain a parameter:

C++
#include "coroutine_function.hpp"
#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>

//
//-----------------------------------------------------------
void longOperarionWithParameter( 
      Tasks::IYieldRef const & _refYield
    , boost::shared_ptr< Parameter > const & _parameter
   )
{
    // ... any actions
} 

// ... any code

//----------------------------------------------------------- 
void ExampleDlg::OnDoOperation()
{
    boost::shared_ptr< Parameter > parameter( new Parameter() );

    // ... any actions with parameter initialization

    boost::shared_ptr< Tasks::IdleCoroutineTask > idleTask( 
            new Tasks::IdleCoroutineTask( 
                boost::bind( &longOperarionWithParameter, _1, parameter )
             )
        );

    theApp.ScheduleIdleTask( idleTask );
}

 

Points of Interest

  1. The parameter Tasks::IYieldRef is defined as boost::reference_wrapper<Tasks::IYield>. This is a workaround for unintended behaviour of references used by boost::bind.
  2. Coroutines should not be used by other coroutines - it may lead to unpredictible results. Unless the code could be adjusted.
  3. In order to use in multittheaded environment, the code should be a-little adjusted (both Tasks::FiberPool and  Tasks::FiberInfo::ms_mainFiber should be changed to thread-local).
  4. The goal of factory method create in class Coroutine is to ensure, that the class will not be instantiated in other ways as accessible by shared pointer, it could lead to unpredictible results.

History

2016-07-27 First version.

2016-07-29 Introduction improvement.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)