【问题标题】:How to prevent memory leaks while cancelling an operation in a worker thread?如何在取消工作线程中的操作时防止内存泄漏?
【发布时间】:2009-01-08 21:15:20
【问题描述】:

目前我正在开发一个包含数学分析的桌面应用程序。我使用 qt 作为 GUI 和用 c++ 编写的项目。 当用户开始分析时,我打开一个工作线程并启动一个进度条。到目前为止一切正常,当用户取消操作时问题开始。操作很复杂,我使用了几个函数和对象,我在几个分配/释放内存次。我想了解在取消操作中我应该如何恢复。因为可能存在内存泄漏。我应该使用哪种模式或方法来保证取消操作的稳健性和安全性?

我的想法是抛出异常,但操作真的很复杂,所以我应该将 try-catch 放到我的所有函数中还是有更通用的方式,模式..

编辑:问题是我的对象在范围之间传输,所以 shared_ptr 或 auto_ptr 不能解决我的问题, 标记想法可以,但我认为它需要这么多代码,应该有一个简单的方法。

【问题讨论】:

  • 我不明白为什么 shared_ptr 不起作用...其目的正是为了在不同范围之间安全地共享对象。
  • 一件重要的事情我还没有看到:如果您当前正在调用操作系统的“终止线程”功能,请不要这样做。您必须安排工作线程自行终止,否则基本上无法防止泄漏。
  • 我加入了 Fabio 的问题:shared_ptr 能够在不同范围之间传输分配的数据。
  • 没有“工作线程 C++”这样的东西 >.

标签: c++ design-patterns qt memory-leaks worker-thread


【解决方案1】:

关闭工作线程的一种非常常见的方法是用一个标志标记它,并让工作线程定期检查这个标志。如果被标记,它应该停止其工作流程,清理并退出。

你的情况有这种可能吗?

【讨论】:

    【解决方案2】:

    工作线程应检查是否有消息停止。消息可以通过标志或事件。当收到停止消息时,线程应该退出。

    对所有分配的内存使用 BOOST 安全指针。退出时你不会有内存泄漏。永远。

    【讨论】:

      【解决方案3】:

      确保您分配的内存是自己的

      确保每个分配的内存都归一个智能指针所有,无论是 C++03 的 auto_ptr、C++11 的 unique_ptr 还是 Boost 的 scoped_ptr,甚至是 shared_ptr(可以共享、复制和移动)。

      这样,RAII 将保护您免受任何内存泄漏。

      使用Boost.Thread 1.37

      阅读 Interrupt Politely,Herb Sutter 的一篇文章,解释了中断线程的各种方法。

      今天Boost.Thread 1.37,您可以通过抛出异常来要求线程终止。在 Boost 中,它是 boost::thread_interrupted 异常,它将从任何 interruption point 抛出异常。

      因此,您无需处理某种消息循环,或验证某些全局/共享数据。主线程通过异常请求工作线程停止,一旦工作线程到达中断点,就会抛出异常。前面描述的 RAII 机制将确保您分配的所有数据都将被正确释放。

      假设您有一些将在线程中调用的伪代码。它可能是一个可能会分配内存的函数,另一个会在循环内进行大量计算:

      Object * allocateSomeObject()
      {
         Object * o = NULL ;
      
         if(/*something*/)
         {
            // Etc.
            o = new Object() ;
            // Etc.
         }
      
         return o ; // o can be NULL
      }
      
      void doSomethingLengthy()
      {
         for(int i = 0; i < 1000; ++i)
         {
            // Etc.
            for(int j = 0; j < 1000; ++j)
            {
               // Etc.
               // transfert of ownership
               Object * o = allocateSomeObject() ;
               // Etc.
               delete o ;
            }
            // Etc.
         }
      }
      

      上面的代码不安全,如果不采取措施确保内存始终由 C++ 对象(通常是智能指针)拥有,则无论中断模式如何都会泄漏。

      可以通过这种方式进行修改,使代码既可中断,又可确保内存安全:

      boost::shared_ptr<Object> allocateSomeObject()
      {
         boost::shared_ptr<Object> o ;
      
         if(/*something*/)
         {
            // Etc.
            boost::this_thread::interruption_point() ;
            // Etc.
            o = new Object() ;
            // Etc.
            boost::this_thread::interruption_point() ;
            // Etc.
         }
      
         return o ; // o can be "NULL"
      }
      
      void doSomethingLengthy()
      {
         for(int i = 0; i < 1000; ++i)
         {
            // Etc.
            for(int j = 0; j < 1000; ++j)
            {
               // Etc.
               // transfert of ownership
               boost::shared_ptr<Object> o = allocateSomeObject() ;
               // Etc.
               boost::this_thread::interruption_point() ;
               // Etc.
            }
      
            // Etc.
            boost::this_thread::interruption_point() ;
            // Etc.
         }
      }
      
      void mainThread(boost::thread & worker_thread)
      {
         // etc.
         if(/* some condition */)
         {
            worker_thread.interrupt() ;
         }
      }
      

      不使用 Boost 吗?

      如果你不使用 Boost,那么你可以模拟这个。如果线程应该被中断,则将一些线程存储布尔变量设置为“true”。添加检查此变量的函数,如果为真,则抛出特定异常。让线程的“根”捕获此异常以使其正确结束。

      免责声明

      我现在无法访问 Boost 1.37,所以我无法测试之前的代码,但想法就在那里。我会尽快对此进行测试,并最终发布更完整/正确/可编译的代码。

      【讨论】:

      • STL 中没有auto_ptr
      • @Lightness Races in Orbit:已更正。
      【解决方案4】:

      您应该尝试将动态分配的资源保存在自动(位于堆栈上的本地)哨兵对象中,当这些资源超出范围时,它们会在其析构函数中释放这些资源。这样您就可以知道它们不会泄漏,即使函数由于异常而退出。您可能还想研究用于在例程之间共享内存的 boost 库 shared_ptr。

      【讨论】:

        【解决方案5】:

        首先,多线程应用程序抛出异常是不确定的,因为没有标准的方法来处理它们(它们会传播到其他线程吗?调度程序?main()?其他地方?)。至少在你得到一个内置标准化线程的 C++0x 库之前。

        目前使用 RAII 更有意义(这将保证在范围退出时清理所有资源(包括内存),无论它是由于成功还是失败而存在)并具有某种状态将代码传递回最有意义的线程(例如调度程序)。

        此外,十多年来一直不鼓励直接取消线程。正如 Simon Jensen 建议的那样,告诉线程停止自己并让线程处理清理工作要好得多。

        【讨论】:

        • 明确一点:不鼓励直接取消线程的原因是因为在几乎所有平台/环境上,这将导致析构函数永远不会被调用,这意味着 RAII 简单地赢了不行。
        • 好吧,这也令人沮丧,因为在计算过程中,一些数据已更改,但其他数据没有。如果该数据 应该被清理(也就是说,如果它在线程外部可见),如果线程被杀死而没有让事情变得正确的机会。
        • 在某些平台上,线程终止时不一定会删除锁。
        【解决方案6】:

        这个问题没有通用的解决方案。

        一些可能的策略:

        • 有时使用 shared_ptrs 和朋友会有所帮助
        • 如果您不希望取消功能使您的算法混乱,请考虑抛出。抓住顶层函数并从那里清理。
        • 无论您放在堆栈上而不是堆上,都不会导致泄漏
        • 对于堆上的大型结构,类之间有很多指针,通常需要严格提供一种方法来释放整个内存结构。
        • 您是否考虑在取消时丢弃的内存池中放置新的?

        但无论如何,要有策略,否则你会感到痛苦。

        【讨论】:

          【解决方案7】:

          答案是取决于您的操作的复杂性。

          这里有几种方法。 1)如前所述,在操作中放置一个“取消”标志,并让该操作定期(关闭)间隔轮询取消标志,可能至少与您更新进度条的频率一样。当用户点击取消时,然后点击取消例程。

          现在,至于在这种情况下的内存处理,我已经完成了几种方法。我的偏好是使用智能指针或 STL 对象,它们会在超出范围时自行清理。基本上,在一个对象中声明你的对象,该对象有一个可以为你处理内存清理的析构函数;当您创建这些对象时,会为您创建内存,并且当对象超出范围时,内存会自动删除。您还可以添加诸如“处置”方法之类的东西来处理内存。它可能看起来像这样:

          class MySmartPointer { 
               Object* MyObject;
               MySmartPointer() { MyObject = new Object(); }
               ~MySmartPointer() { if (MyObject != null) { delete MyObject; MyObject = null; }}
               void Dispose() { if (MyObject != null) { delete MyObject; MyObject = null; } }
               Object* Access() { return MyObject; }
           }
          

          如果您想变得非常聪明,您可以将该类模板化为任何对象的通用模板,甚至可以包含数组等。当然,您可能必须在访问对象之前检查它是否已被释放,但是当您直接使用指针时它们会中断。您也许还可以内联 Access 方法,这样在执行期间就不会花费您一个函数调用。

          2) goto 方法。前面声明你的内存,最后delete,当你打cancel方法的时候,调用goto去到方法的最后。我认为某些程序员可能会为此对你处以私刑,因为 goto 被认为是非常糟糕的风格。因为我学习了基本和“goto 10”作为循环的一种方式,所以它并没有让我害怕,但你可能需要在代码审查期间回答一个学究,所以你最好有一个很好的解释为什么你选择这个而不是选项 1。

          3) 把它全部放到一个进程中,而不是一个线程中。如果可以,将所有信息序列化到磁盘上,然后在另一个程序中运行复杂的分析。如果该程序死了,就这样吧,它不会破坏您的主应用程序,如果您的分析如此复杂并且在 32 位机器上,您可能需要所有内存空间才能运行。而不是使用共享内存来传递进度信息,您只需将进度读/写到磁盘,并且取消是即时的。实现起来有点棘手,但并非不可能,而且可能更稳定。

          【讨论】:

            【解决方案8】:

            由于您使用的是 Qt,您可以利用 QObject 的父内存系统。

            您说您在工作线程运行期间分配和释放内存。如果每个分配都是 QObject 的一个实例,为什么不把它作为当前 QThread 对象的父对象呢?

            MyObject * obj = new MyObject( QThread::currentThread() );
            

            您可以随时删除它们,这很好,但如果您遗漏了一些,它们将在 QThread 被释放时被清除。

            请注意,当您告诉您的工作线程 QThread 取消时,您必须等待它完成才能删除 QThread 实例。

            workerThread->cancel();  // reqfuest that it stop
            workerThread->wait();    // wait for it to complete
            delete workerThread;     // deletes all child objects as well
            

            如果你使用 Simon Jensen 的回答来退出你的线程和 QObject 作为你的内存策略,我认为你会处于一个很好的情况。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2010-12-20
              • 1970-01-01
              • 2011-08-09
              • 2012-03-30
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多