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:
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:
BOOL ExampleApp::OnIdle( LONG _counter )
{
BOOL moreFrameworkTasks = CWinApp::OnIdle( _counter );
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:
- The MFC application should be created (or Qt with possibility to adjust the code).
- The downloaded code should be included into the project
- The application class should be modified in similar manner to ExampleApp
Later it could be done in the following way:
#include "coroutine_function.hpp"
#include <boost/shared_ptr.hpp>
void longOperarion( Tasks::IYieldRef const & _refYield )
{
bool done = false;
while( !done )
{
_refYield.get().yield();
std::ifstream file( getNextFileName(), std::ifstream::in );
while( !file.eof() )
{
std::string line;
std::getline( file, line );
_refYield.get().yield(); }
}
}
void ExampleDlg::OnDoOperation()
{
boost::shared_ptr< Tasks::IdleCoroutineTask > idleTask(
new Tasks::IdleCoroutineTask( &longOperarion )
);
theApp.ScheduleIdleTask( idleTask );
}
Moreover, the scheduled function can contain a parameter:
#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
)
{
}
void ExampleDlg::OnDoOperation()
{
boost::shared_ptr< Parameter > parameter( new Parameter() );
boost::shared_ptr< Tasks::IdleCoroutineTask > idleTask(
new Tasks::IdleCoroutineTask(
boost::bind( &longOperarionWithParameter, _1, parameter )
)
);
theApp.ScheduleIdleTask( idleTask );
}
Points of Interest
- 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
. - Coroutines should not be used by other coroutines - it may lead to unpredictible results. Unless the code could be adjusted.
- 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). - 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.