响应式 GUI 应用程序技巧






3.90/5 (10投票s)
如何避免在GUI应用程序中阻塞长时间操作,导致应用程序挂起。
引言
通常,在协程中实现长时间操作(作为空闲任务处理)会更好。 可下载的代码中提供了对该解决方案的支持。
背景
在GUI应用程序中阻塞长时间操作(例如高级数据解析)是令人讨厌的,它们会导致窗口冻结和其他GUI元素。 此代码的目的是确保在同一线程中进行非阻塞的长时间操作,而无需考虑多线程问题。
例如,在MFC框架中有一个可重写的OnIdle
方法。 在此解决方案中,可以使用来自关联源的接口Tasks::IIdleTask
来重写它。
// ...
//=============================================================================
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 ); // 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 )
}
通常,应派生实现接口Tasks::IIdleTask
的类,应重写run
方法。 isDone
方法应指示操作是否完成。
但这个想法更进了一步。 类Tasks::IdleCoroutineTask
(实现接口Tasks::IIdleTask
)可用于将一个可让步协程安排为空闲任务。 在这种情况下,一个合适的函数可以在“行之间”进行任何长时间操作,同时处理GUI消息。 但是,计划的函数负责适当地频繁调用接口Tasks::IYield
的yield
方法,该方法作为参数传递。 否则,GUI行为可能会受到干扰,或者至少受到影响。
Tasks::IYield::yield
方法可以在两种模式下调用
- 启发式 - 仅当协程使用了至少200毫秒的CPU时,才会发生让步,因此,过度调用比很少调用要好。
- 无条件 - 总是会发生让步
无论空闲时间处理如何,协程在其他问题中都是一个有用的实用程序,例如智能迭代器。 要显式使用协程,应使用Tasks::Coroutine
类,它仅由静态create
方法实例化。 它不会自动将控制权传递给协程,而应使用switchTo
方法。
在Windows中,协程基于fiber系统机制。 由于每个fiber都使用其自己的堆栈,因此频繁创建fiber可能会影响系统性能。 因此,实现了fiber的池; 每次不是创建和删除fiber,而是始终保持随时可用的fiber。 创建协程时,从池中获取一个fiber并关联,以后在协程完成后将其回收。
使用代码
先决条件
- 应创建MFC应用程序(或Qt,并可以调整代码)。
- 应将下载的代码包含到项目中
- 应以类似于ExampleApp的方式修改应用程序类
稍后可以按以下方式完成
#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 );
}
此外,计划的函数可以包含一个参数
#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 );
}
关注点
- 参数
Tasks::IYieldRef
定义为boost::reference_wrapper<Tasks::IYield>
。 这是一个针对boost::bind
使用的引用的意外行为的解决方法。 - 协程不应被其他协程使用 - 这可能会导致不可预测的结果。 除非可以调整代码。
- 为了在多线程环境中使用,应稍作调整代码(
Tasks::FiberPool
和Tasks::FiberInfo::ms_mainFiber
都应更改为线程局部变量)。 - 类
Coroutine
中工厂方法create
的目标是确保该类不会以共享指针可访问的其他方式实例化,这可能会导致不可预测的结果。
历史
2016-07-27 第一个版本。
2016-07-29 介绍改进。