65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (16投票s)

2003 年 3 月 22 日

CPOL

12分钟阅读

viewsIcon

63683

downloadIcon

663

本文介绍了一个用于挂钩空闲处理的便捷类。

引言

本文将介绍一个便捷的类,用于处理长流程而不会导致应用程序冻结的情况。当实现一个线程不是处理此问题的最佳方法时,它非常适用。

背景

1995 年,在尝试开发 Windows 模拟器应用程序时,我们遇到的一个障碍是长流程。创建处理方法没有问题,但一旦进入循环,主应用程序线程在循环完成并返回控制之前将不再处理 UI 消息。如果这超过几秒钟,很快就会成为一个问题,用户将绝望地敲击键盘或单击鼠标以尝试中止。当时,线程在设计中不是一个选项,因此必须找到另一种解决方案。

问题

在处理长流程时,最常见的情况是它们是由密集型循环引起的,并且通常是嵌套循环。一旦进入循环,直到整个过程完成才能退出。有时,这个过程需要几分钟甚至几个小时才能完成。在此期间,应用程序无法处理任何消息,窗口按钮、控件、工具栏和按键都变得毫无用处。要中止,您需要处理其中一个的消息,这就需要应用程序在某个时候返回消息循环。

为什么不用线程?

  1. 通常,程序代码不需要在完全独立的线程中运行。相反,需要的是一种手段来简单地“推迟”执行,直到应用程序不太繁忙。这可以使主线程保持清晰和响应迅速。
  2. 很多时候,代码需要在没有干预的情况下运行特定部分(就像线程中的 CRITICAL section)。由于任务在主线程中运行,因此没有干预,您无需定义这些锁。
  3. 如果您需要访问资源或需要进行绘制,可以在线程中完成,但在 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。出于维护原因,我们使用自己的集合类。此处提供的代码有很好的文档记录,应该易于理解。

如果您有任何想法,请在下面的评论中告诉我。祝您愉快!

© . All rights reserved.