如何在取消工作线程中的操作时防止内存泄漏?

时间:2009-01-08 21:15:21

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

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

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

编辑:问题是我的对象是在作用域之间转移的,所以shared_ptr或auto_ptr不能解决我的问题, 国旗的想法可以,但我认为它需要这么多的代码,应该有一个简单的方法。

8 个答案:

答案 0 :(得分:9)

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

这种可能性适用于您的情况吗?

答案 1 :(得分:4)

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

USE BOOST分配所有内存的安全指针。在退出时你将没有内存泄漏。如初。

答案 2 :(得分:2)

确保已分配的内存已归属

确保每个分配的内存都由智能指针拥有,可以是C ++ 03的auto_ptr,C ++ 11的unique_ptr或Boost的scoped_ptr,甚至是shared_ptr(可以共享,复制和移动)。< / p>

这样,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,那么您可以模拟它。如果线程应该被中断,请将一些线程存储类似于boolean的变量设置为“true”。添加检查此变量的函数,如果为true则抛出特定异常。让线程的“根”捕获此异常以使其正确结束。

声明

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

答案 3 :(得分:0)

您应该尝试在自动(堆栈中存在的本地)中保留动态分配的资源,这些资源在超出范围时会在其析构函数中释放这些资源。通过这种方式,您可以知道它们不会泄漏,即使函数因异常而退出也是如此。您还可能需要调查boost库shared_ptr以在例程之间共享内存。

答案 4 :(得分:0)

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

目前使用RAII更有意义(它可以保证所有资源 - 包括内存 - 在范围退出时被清除,是否由于成功或失败而存在)并具有某种状态代码传递回最合适的线程(例如调度程序)。

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

答案 5 :(得分:0)

这个问题没有一般解决方案。

一些可能的策略:

  • 有时使用shared_ptrs和朋友有帮助
  • 如果您不希望取消功能弄乱您的算法,请考虑投掷。抓住顶层功能并从那里清理。
  • 无论你放在堆栈上而不是堆上,都不会造成泄漏
  • 对于堆上的大型结构,类之间有很多指针,通常是一个严格提供解除分配整个内存结构的方法的问题。
  • 您是否考虑过在取消时丢弃的内存池中放置新内容?

但无论如何,有策略或者你会感到痛苦。

答案 6 :(得分:0)

答案是,这取决于您的操作的复杂程度。

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

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

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方法。在前面声明你的内存,在最后删除,当你点击cancel方法时,调用goto去到方法的结尾。我认为某些程序员可能会因此而嘲笑你,因为goto被认为是非常糟糕的风格。由于我在基础和'goto 10'上学习循环,它并没有让我感到惊讶,但你可能不得不在代码审查期间回答一个学究,所以你最好有一个很好的解释为什么你选择这个而不是选项1.

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

答案 7 :(得分:0)

由于您使用的是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作为你的记忆策略,我认为你会处于良好状态。