用于长时间进程的便捷空闲处理类






4.89/5 (16投票s)
本文介绍了一个用于挂钩空闲处理的便捷类。
引言
本文将介绍一个便捷的类,用于处理长流程而不会导致应用程序冻结的情况。当实现一个线程不是处理此问题的最佳方法时,它非常适用。
背景
1995 年,在尝试开发 Windows 模拟器应用程序时,我们遇到的一个障碍是长流程。创建处理方法没有问题,但一旦进入循环,主应用程序线程在循环完成并返回控制之前将不再处理 UI 消息。如果这超过几秒钟,很快就会成为一个问题,用户将绝望地敲击键盘或单击鼠标以尝试中止。当时,线程在设计中不是一个选项,因此必须找到另一种解决方案。
问题
在处理长流程时,最常见的情况是它们是由密集型循环引起的,并且通常是嵌套循环。一旦进入循环,直到整个过程完成才能退出。有时,这个过程需要几分钟甚至几个小时才能完成。在此期间,应用程序无法处理任何消息,窗口按钮、控件、工具栏和按键都变得毫无用处。要中止,您需要处理其中一个的消息,这就需要应用程序在某个时候返回消息循环。
为什么不用线程?
- 通常,程序代码不需要在完全独立的线程中运行。相反,需要的是一种手段来简单地“推迟”执行,直到应用程序不太繁忙。这可以使主线程保持清晰和响应迅速。
- 很多时候,代码需要在没有干预的情况下运行特定部分(就像线程中的 CRITICAL section)。由于任务在主线程中运行,因此没有干预,您无需定义这些锁。
- 如果您需要访问资源或需要进行绘制,可以在线程中完成,但在 MFC 中,这是一件麻烦事,因为您必须附加和分离窗口和资源以保持内部表的同步。您不需要使用空闲任务对象来执行此操作。
解决方案
在早期的开发工作中,我们直接使用 Windows API 开发了所有内容。这意味着我们也必须创建消息循环。由于我们必须返回到消息循环,因此解决方案就存在于循环本身。与其为过程创建一个大的外部循环,不如利用消息队列已提供的轮询来为长流程提供外部支持。所有需要做的就是挂钩到它并执行一个循环测试来决定何时完成。我们编写了 C++ 应用程序对象来管理消息循环及其循环任务处理。创建了更小的对象,称为“tasks
”,以便在需要时挂钩到循环。
2002 年,当 MFC 集成到我们的软件中时,主应用程序类在名为 OnIdle()
的 virtual
方法中提供了相同的基本支持。这很好,因为它意味着最初用于此目的的代码可以被删除,现在只需要将挂钩代码引入空闲周期,而无需干扰 MFC 消息循环。这尤其具有吸引力,因为它意味着当应用程序最近移植到使用 MFC 时,我们可以使用几乎相同的代码。此外,由于我们没有诉诸多个消息处理点,因此所有窗口消息都可以保留在 MFC 的控制之下。
尽管提供了空闲处理调用,但它并不方便。没有好的方法可以挂钩和分离它。我们需要的是一种结构和一个简单的对象,让开发人员能够创建可以方便地添加和删除的代码集。这样,执行块就可以被“打包”起来,并通过一个干净的接口进行处理,该接口与线程对象非常相似。为此创建的类称为 **ZTask
**。我们库中的所有内容都以 Z
作为前缀,但您可以将其更改为任何您喜欢的名称。这个“task
”对象构成了本次讨论的基础。
要使用空闲处理类,只需要对派生的 CWinApp
类(在本例中称为 MyApp
)进行简单的更改。一旦所有任务都表明完成,此方法将返回 false
,并使应用程序休眠直到下一个消息到达。这是在应用程序中启用任务处理所需的唯一更改。
//-< OVERRIDE >-------------------------------------------------------------
// OnIdle()
//
// When the message queue is empty, this is time to work on stuff in the
// background. These background activities, or Tasks are called to execute
// one cycle of the work and then return to the queue.
//-------------------------------------------------------------------------
BOOL MyApp::OnIdle(LONG count)
{
// From the MSDN C/C++ docs on the logical OR (||) operation, this code is
// sensitive to "short-circuit evaluation" of the C/C++ logical AND (&&)
// and OR (||) operators. This means super::OnIdle() is guaranteed to
// execute first and the latter is only called if the first returns FALSE.
return CWinApp::OnIdle(count) || ZTask::executeTasks();
}
一旦通过源代码中的特定方法将任务添加到应用程序,应用程序就会自动管理这些对象。任务在每个空闲调用中执行一次,并一直持续到所有任务完成。您所需要做的就是创建这些对象来完成您的工作,并通过将它们添加到应用程序来启动它们。
任务类
现在我们已经在应用程序中获得了支持,定义要执行的工作变得清晰而简单。我们创建了一个名为 ZTask
的任务对象,用于将单个任务打包成精美的束。该类具有 virtual
方法,这些方法在首次添加时、每次调用空闲时以及移除时都会被调用。处理任务单个迭代的方法称为 execute()
。在派生任务中必须重写它。另外两个是可选的。execute()
方法将在空闲循环中被持续调用,直到您返回 true
。一旦返回 true
,任务对象将被清理并根据需要删除。您所要做的就是决定在单次传递中执行多少处理。这将影响应用程序的响应能力。
//----------------------------------
// Class methods
//----------------------------------
public:
static bool addTask(ZTask *task);
static bool removeTask(ZTask *task);
static bool executeBlocking(ZTask *task);
提供了两种响应方法,它们在将 taskAdded
/Removed()
添加/从处理队列中移除时被调用。这些方法在发生时立即被调用,并提供了设置长流程的机会。任何必须初始化的内容都放在这些方法中,以便在调用 execute
方法时做好准备。添加/移除方法只调用一次,因此例如在文件读取中,这通常会用于打开和验证要读取的文件。
静态方法 addTask()
负责设置任务并使其准备执行。这包括执行一次健全性检查,以确保我们在应用程序线程中运行,并确保任务不是子任务(即顶级任务)。默认情况下,它们都是顶级任务。
//==============================================================================
// Add/Remove registered tasks
//------------------------------------------------------------------------------
bool ZTask::addTask(ZTask *task)
{
ASSERT(AfxGetThread() == AfxGetApp()); // Application thread context only!
// If the parent is already executing in blocking mode, then
// we must execute the child task in blocking mode as well.
// Short circuit the method at this point by returning.
//
ZTask *parent = task->pParent;
if (parent != nullptr && parent->bBlocking)
{
return ZTask::executeBlocking(task);
}
// Add the task to the main execution list, but do not add the task
// if it is already there! By merging the task with the list it
// will return false if it is found and skip over.
//
ASSERT(parent == nullptr || 0 <= vTasks.indexOf(parent)); // Sanity check!
if (vTasks.mergeElement(task))
{
// If a task has a parent, then by design the parent must wait
// for the child task. If so interrupt the parent by incrementing
// the parent lock count.
//
if (parent != nullptr)
{
parent->interrupt();
parent->vChildren << task;
}
// Ensure that the tasking is not locked for add/remove. New tasks
// should not be added while nested in another add/remove action.
//ASSERT(!bAdding); // Check the call stack!
bAdding = true;
task->bEscape = task->taskAdded();
if (task->bEscape)
TRACE("\n[%s] Bad initial state, terminating task."
, task->getName());
// If no tasks are present. the idle processing
// may be inactive. Post a dummy message to kick
// start an initial OnIdle() method call.
CWnd *main_wnd = AfxGetMainWnd();
if (main_wnd != nullptr && vTasks.size() == 0)
main_wnd->PostMessage(WM_KICKIDLE);
bAdding = false;
return true;
}
return false;
}
任务对象本身有三个 virtual
方法用于执行任务。
//----------------------------------
// Virtual methods
//----------------------------------
protected:
virtual bool taskAdded();
virtual void taskRemoved();
virtual bool execute();
这些响应方法中的每一个都有一个默认实现,它将尝试运行实现简单接口的对象中的代码。此时,我们暂时忽略可运行对象,只考虑创建一个派生任务。virtual
方法将在任务添加和移除时被调用。我将任务视为一个“三明治”,其中面包是开始/停止活动,而肉是过程执行。
//-< VIRTUAL >------------------------------------------------------------------
// taskAdded()/Removed()
//
// The task has been added to the execute list, no action by default. These
// methods are called prior to the first execution and after the last execution.
// If specific initial/final actions are required, they can be overridden.
//------------------------------------------------------------------------------
bool ZTask::taskAdded()
{
// Some tasks may not have derived implementations, so this method would
// not be overridden. In this case the default action is execute methods
// from an attached object implementing IRunnable.
//
if (pRunnable != nullptr)
pRunnable->initialize(false);
return false; // No errors.
}
void ZTask::taskRemoved()
{
// Some tasks may not have derived implementations, so this method would
// not be overridden. In this case the default action is execute methods
// from an attached object implementing IRunnable.
//
if (pRunnable != nullptr)
pRunnable->finalize();
}
由于任务在应用程序主线程的空闲处理中执行,因此无法预测任务何时完成。如果任务是使用 new
在堆上创建的,那么它也必须被删除。与线程一样,task
对象有一个可以设置的标志,告诉它在任务完成后自动删除自身,这样您就不必这样做。当任务完成时,它会被放入一个回收列表,直到下一个空闲周期发生时才会被删除。这由 static
方法 executeTasks()
自动处理,该方法如上所述,在 OnIdle()
中调用。所有操作都在这里发生。
//------------------------------------------------------------------------------
// executeTasks()
//
// When the message queue is empty, the is time to work on stuff in the
// background. These background activities, or Tasks are called to execute
// one cycle of the work and then return to the queue.
//------------------------------------------------------------------------------
bool ZTask::executeTasks()
{
// If there is anything in the recycling bin, delete those
// objects before proceeding with any other tasks.
//
int n = ZTask::vRecycle.size();
while (n--)
delete ZTask::vRecycle[n];
vRecycle.removeAll();
// If the base class idle work is done, then the task count
// will have dropped to zero. If so, then there are no more
// tasks and return. On return it will signal OnIdle() that
// no more idle cycles are required.
//
int task_count = ZTask::vTasks.size();
if (task_count == 0)
{
return false; // All done with tasks!
}
// Otherwise check if reached the end of the
// task list an start over at the top.
else if (iIndex >= task_count)
{
bNextIdle = bDoIdle; // Establish critical polling.
iIndex = 0; // Start from the top of the list.
bDoIdle = false; // Begin next polling check.
}
// If we are at the top of the list, sort the list of tasks
// based on the priority levels, this is useful for one-shot
// tasks where some are preferred to be executed first, even
// though they are added later to the queue.
if (bSort==true && iIndex == 0)
{
vTasks.sort(comparePriority);
}
// Get the next task in the list. Verify it exists.
ZTask *task = ZTask::vTasks[iIndex];
if (task == nullptr)
{
ASSERT(task != nullptr); // Sanity check!
return false; // Something went wrong.
}
// If the task is active, then its lock count
// will be zero. Check this before processing.
if (!task->iLockCount || task->iStepCount)
{
if (task->iStepCount)
task->iStepCount--;
// If the task is ready, call the execute
// method. Enclose in a try/catch block to
// prevent a throw from aborting the program.
try
{
if (task->ready() && !task->bEscape)
{
task->bEscape = task->execute();
}
}
catch (CException *x)
{
x->ReportError();
x->Delete();
task->bEscape = true;
}
// If there is a termination request or a task returns true and
// no children are present, remove it from the task list,
// causing the list to collapse down to the next task.
if ((task->bTerminate==true || task->bEscape==true) && task->iLockCount==0)
{
ZTask::removeTask(task);
iIndex--; // Collapse indexer.
}
// Otherwise check if this task is marked as critical
// polling. This will enforce idle cycles from the OS
// on the next pass of the list.
else
bDoIdle = bDoIdle || task->bCritical;
}
iIndex++; // Increment to the next task.
// Returns 'true' if there is critical processing flagged
// from the last pass down the list. If not it will be
// 'false' and this procedure will not be called until
// the next window message has been processed, freeing up
// the CPU.
return bNextIdle;
}
这里有一些家政和验证步骤,每个步骤都旨在处理循环的特定细节。总的来说,它们大多是内部函数。virtual execute()
方法与 taskAdded()
/taskRemoved()
不同,它会被反复调用,直到返回“true
”值,表示它已完成处理,任务现在可以被移除了。为了使这项工作具有协作性并保持 UI 响应,长流程的外部循环被移除,并替换为对 execute()
的重复调用,直到它告诉调用者终止。
//-< VIRTUAL >------------------------------------------------------------------
// execute()
//
// Override this method to insert the main body of execution for the task.
// Since a task is hooked into the idle time of the message queue, this
// method is called in a single idle cycle and thus comprises a single
// iteration. The execute action should be chosen such that it does not block
// the application.
//------------------------------------------------------------------------------
bool ZTask::execute()
{
// When sub-classing, this method should be overridden to extend
// the behaviour and include process activities. Otherwise use an
// attached object implementing IRunnable.
//
if (pRunnable)
return pRunnable->run();
ASSERT(0); // Oops! You must override this or nothing useful will happen!
return true;
}
创建和使用任务现在非常简单方便。由于任务现在提供了外部循环,您只需要决定如何在重写的任务执行中分解长流程。
使用任务
对象的使用很大程度上取决于您的需求。如果您想多次调用任务,例如更新器,您可能希望构造一个嵌入了自动删除标志的对象,该标志设置为 false
。当您需要启动它时,可以像这样调用
ZTask::addTask(this->pUpdater);
当它完成时(通过返回 true
),它将自动再次删除。我们使用此策略来更新组件的后端存储。如果您想创建一个任务然后忘记它,您可能会动态创建它,然后立即启动它。在这种情况下,自动删除标志将被打开,以便在对象完成时进行清理。调用可能看起来像这样。
ZTask::addTask(new MyTask());
创建派生任务类时,至少需要实现 execute()
方法。taskAdded()
/taskRemoved()
是可选的,如果未实现,将执行默认行为。就我个人而言,我通常会实现所有三个 virtual
方法以求完整。
将 execute()
分成更小的部分只是一个判断问题。我喜欢将其设置为每次迭代通常为 100 毫秒或更短,这样任何用户输入将在不到这个时间就被响应。例如,如果我有一个大的 ASCII 文件读取任务,我通常会一次读取和处理一行或一小组数据,并在每次迭代中跟踪我的位置。通常,我会使用进度条来指示其当前位置,从而让我了解其进展情况。
阻塞执行
当然,本文是关于不阻塞应用程序的,但有时它必须是这种情况。我遇到的一个情况是,相同的代码在一种情况下不阻塞,但在其他情况下必须阻塞。此外,我非常喜欢通过阻塞模式运行我的代码来测试它,以确保它在挂钩到空闲队列之前按预期运行。为了完整起见,我包含了一个附加的 static
方法 executeBlocking()
。
//==============================================================================
// executeBlocking()
//
// Execute a task, blocking until done. Normally this is not intuitive since
// the whole point of having a task is *not* to block. In some situations it
// is useful for a task to execute this way for both development testing and
// to provide the option without re-write of the task.
//
//------------------------------------------------------------------------------
bool ZTask::executeBlocking(ZTask *task)
{
if (task != nullptr)
{
task->bBlocking = true; // Blocking flag must be set here.
// Call the initialization method. If it returns true, then the
// initial state is bad and we must exercise an escape action
// from the task. Finalization only occurs if initialization
// is successful.
//
task->bEscape = task->taskAdded();
if (task->bEscape == false)
{
do {
if (task->iStepCount)
task->iStepCount--;
if (task->ready() && !task->bEscape)
{
task->bEscape = task->execute();
}
} while (!task->bEscape);
}
else
{
TRACE("\n[%s] Bad initial state, terminating task."
, task->getName());
}
task->taskRemoved();
// Like thread objects, check flag to have the object put in
// the recycling bin. We use a recycle bin so that the object
// can linger until the next idle cycle. Lingering allows the
// task to be accessed after this call to retrieve state
// information and error messages.
//
if (task->bAutoDelete)
{
ZTask::vRecycle << task;
}
return true;
}
return false;
}
此方法将以与 executeTasks()
相同的模式运行代码,但没有空闲周期切片。它仍然使用回收列表进行析构,但会等到下一个空闲周期。这种“滞留
”可能很方便,因为调用后对象仍然有效,并且可以查询状态和变量信息。
例如,我使用任务来打包组件工厂,这些工厂在处理 XML 文档元素时生成 UI 设备。代码如下所示
Scopes::Instrument::Factory factory(this);
factory.setInput(&element);
ZTask::executeBlocking(&factory);
c = factory.pInstrument;
此示例使用了一个作用域为局部的工厂任务,但它也可以创建在堆上并设置自动删除标志,以便稍后进行回收。在任何时候,通过将 executeBlocking()
切换为 addTask()
,都可以将同一个任务挂钩到空闲队列。对于代码的验证来说非常方便。
一次性任务
一次性任务是需要异步执行的代码块,但只需要一个通过。这通常是处理消息(如绘图或按钮点击)时发生的情况。task
对象也可以以完全相同的方式使用。这里最大的优点是与任务相关的所有代码都可以打包在一起,而不是分散在窗口消息中。任务所做的就是第一次通过返回 true
。然后应用程序对象会立即将其移除。如果 task
被嵌入为另一个类的成员,它不会被自动删除,并且可以通过随时将其添加回应用程序来重复使用。
在我们的应用程序中,一次性任务被广泛用作树的更新对象以及控件和图表的后端存储。我们不希望这些活动消耗 CPU 时间,直到消息队列空闲为止,从而保持应用程序的响应能力。由于显示表面上可能存在数十个这样的对象,与它们的交互会触发许多更新任务,这些任务会排队等待,直到应用程序空闲。这些对象是我们工具库的一部分,因此它们必须能够自动添加任务,而无需额外的编码。
这是一个处理从通信端口接收的数据包的一次性任务。
ZTask::addTask(new Comm::ExchangeHandler(Comm::Exchange::validate((CPtr)lparam)));
创建通信处理程序,传递通信对象,然后将其交给任务处理,作为一次性任务执行。一旦完成,任务将被放入回收箱,并在下一个空闲周期进行清理。这里有很多事情在进行,但我能够用一行代码调用它。这创建了一个非常易于维护和可扩展的设置。
我发现这种方法的一个附带好处是,它永远不会在窗口滚动时运行,因此不会干扰用户交互。当 UI 变得卡顿时,一个耗时的过程可能会非常明显,而使用这种方法,UI 始终具有优先级,任务将等待用户。
注释
此类利用了另一篇文章中描述的 Vector
类,该文章已由我发布。如果您不想使用 Vector
类,可以轻松地将其替换为 CTypedPtrList
。出于维护原因,我们使用自己的集合类。此处提供的代码有很好的文档记录,应该易于理解。
如果您有任何想法,请在下面的评论中告诉我。祝您愉快!