Как предотвратить утечки памяти при отмене операции в рабочем потоке? - PullRequest
2 голосов
/ 09 января 2009

В настоящее время я работаю над настольным приложением, которое состоит из математического анализа. Я использую qt для GUI и проекта, написанного на c ++. Когда пользователь запускает анализ, я открываю рабочий поток и запускаю индикатор выполнения. До сих пор все нормально, проблема начинается, когда пользователь отменяет операцию. Операция сложная, я использую несколько функций и объектов, я выделяю / освобождаю память в нескольких раз. Я хочу узнать, что я должен сделать для восстановления в операции отмены. Потому что могут быть утечки памяти. Какой шаблон или метод я должен использовать, чтобы быть надежным и безопасным для отмены операции?

Моя идея - выбросить исключение, но операция действительно сложна, поэтому я должен применить try-catch ко всем своим функциям или есть более общий способ, шаблон ..

Редактировать: проблема в том, что мои объекты передаются между областями, поэтому shared_ptr или auto_ptr не решают мою проблему, Идея флага может быть, но я думаю, что для этого нужно так много кода, и должен быть простой способ.

Ответы [ 8 ]

9 голосов
/ 09 января 2009

Довольно распространенный способ закрытия рабочих потоков - пометить его флагом и позволить рабочему потоку проверять этот флаг через регулярные промежутки времени. Если он отмечен, он должен прекратить свой рабочий процесс, очистить и выйти.

Это возможно в вашей ситуации?

4 голосов
/ 09 января 2009

Рабочий поток должен проверить, чтобы сообщение было остановлено. сообщение может быть через флаг или событие. когда получено сообщение об остановке, поток должен выйти.

ИСПОЛЬЗУЙТЕ безопасные указатели BOOST для всей выделенной памяти. на выходе у вас не будет утечек памяти. когда-либо.

2 голосов
/ 10 января 2009

Убедитесь, что выделенная память принадлежит

Убедитесь, что каждая выделенная память принадлежит интеллектуальному указателю, либо auto_ptr в C ++ 03, unique_ptr в C ++ 11 или scoped_ptr в Boost, либо даже shared_ptr (который может использоваться совместно, копироваться и перемещаться) .

Таким образом, RAII защитит вас от любой утечки памяти.

Использование Boost.Thread 1,37

Читать Вежливо прерывать , статья Херба Саттера, объясняющая разные способы прерывания потока.

Сегодня wth Boost.Thread 1.37 . Вы можете попросить завершить поток, выдав исключение. В Boost это исключение boost :: thread_interrupted, которое генерирует исключение из любой точки прерывания .

Таким образом, вам не нужно обрабатывать какой-либо цикл обработки сообщений или проверять некоторые глобальные / общие данные. Основной поток просит рабочий поток остановиться через исключение, и как только рабочий поток достигает точки прерывания, генерируется исключение. Механизм 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, поэтому я не могу протестировать предыдущий код, но идея есть. Я протестирую это как можно скорее и в конце концов опубликую более полный / правильный / компилируемый код.

0 голосов
/ 11 января 2009

Поскольку вы используете 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

Если вы используете ответ Саймона Дженсена, чтобы выйти из своей цепочки, а QObject как своей стратегии памяти, я думаю, вы окажетесь в хорошей ситуации.

0 голосов
/ 09 января 2009

Ответ в том, что это зависит от сложности вашей операции.

Здесь есть несколько подходов. 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. Объявите вашу память спереди, удалите в конце, а когда вы нажмете метод отмены, вызовите goto, чтобы перейти к концу метода. Я думаю, что некоторые программисты могут линчевать вас за это, поскольку goto считается крайне плохим стилем. Поскольку я научился основам и «переходу на 10» как способ зацикливания, это меня не сильно пугает, но вам, возможно, придется ответить педанту во время проверки кода, так что вам лучше дать действительно хорошее объяснение. почему вы выбрали именно этот вариант, а не вариант 1.

3) Поместите все это в процесс, а не в поток. Если вы можете, сериализуйте всю информацию для манипулирования на диск, а затем запустите ваш сложный анализ в другой программе. Если эта программа умирает, пусть она не разрушит ваше основное приложение, и если ваш анализ настолько сложен и на 32-битной машине, вам может понадобиться все это пространство памяти для запуска в любом случае. Вместо использования общей памяти для передачи информации о прогрессе, вы просто читаете / записываете прогресс на диск, и отмена происходит мгновенно. Это немного сложнее в реализации, но не невозможно и потенциально гораздо более стабильно.

0 голосов
/ 09 января 2009

Нет общего решения этого вопроса.

Некоторые возможные стратегии:

  • Иногда использование shared_ptrs и друзей помогает
  • Если вы не хотите, чтобы функциональность отмены загромождала ваш алгоритм, рассмотрите возможность броска. Поймай функцию верхнего уровня и убери оттуда.
  • Все, что вы положите в стек, а не в кучу, не приведет к утечкам
  • Для больших структур в куче с большим количеством указателей между классами обычно возникает вопрос о строгом обеспечении способа освобождения всей вашей структуры памяти.
  • Рассматривали ли вы размещение новых в пулах памяти, которые вы выбрасываете при отмене?

Но в любом случае имейте стратегию, иначе вы почувствуете боль.

0 голосов
/ 09 января 2009

Во-первых, выбрасывать исключения для многопоточных приложений нелепо, потому что нет стандартного способа их обработки (распространяются ли они на другие потоки - планировщик? Main () - где-то еще?). По крайней мере, до тех пор, пока вы не получите библиотеку C ++ 0x, в которую встроены стандартизированные потоки.

В настоящее время имеет больше смысла использовать RAII (который гарантирует, что все ресурсы - включая память - очищаются при выходе из области действия, независимо от того, существует ли она в результате успеха или сбоя) и имеют какой-то статус код, передаваемый обратно тому потоку, который имеет наибольшее значение (например, планировщик).

Кроме того, прямое аннулирование тем не поощрялось более десяти лет. Гораздо лучше сказать, чтобы нить остановилась, и чтобы нить выполняла очистку, как предлагает Саймон Дженсен.

0 голосов
/ 09 января 2009

Вам следует попытаться удерживать динамически распределяемые ресурсы в автоматических (локальных объектах, находящихся в стеке) сторожевых объектах, которые освобождают эти ресурсы в своих деструкторах, когда они выходят из области видимости. Таким образом, вы можете знать, что они не будут протекать, даже если функция выходит из-за исключения. Возможно, вы захотите исследовать дополнительные библиотеки shared_ptr для разделения памяти между подпрограммами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...