繁体   English   中英

在另一个线程上安全地释放资源

[英]Safely release a resource on a different thread

我有一个类似的类:

class A{
    private:
    boost::shared_ptr< Foo > m_pFoo;
}

A的实例在GUI线程上被销毁,它们可以保存对Foo的最后一个引用。 Foo的析构函数可能长时间运行,导致我的GUI线程出现意外暂停。 在这种情况下,我想让Foo在一个单独的线程上被销毁,Foo是自包含的,并且它们立即被释放并不重要。

目前,我使用这样的模式:

A::~A(){
    auto pMtx = boost::make_shared<boost::mutex>();
    boost::unique_lock<boost::mutex> destroyerGate(*pMtx);
    auto pFoo = m_pFoo;
    auto destroyer = [pMtx,pFoo](){
        boost::unique_lock<boost::mutex> gate(*pMtx);
    };

    m_pFoo.reset();
    pFoo.reset();
    s_cleanupThread->post(destroyer);
}

基本上,在lambda中捕获它并锁定直到从对象释放。 有没有更好的方法来实现这一目标? 这似乎比它需要的更复杂。

正如Mark Ransom在评论中已经建议的那样,你可以使用一个专用的破坏线程来从工作队列中获取被破坏的对象,然后将它们简单地丢弃在地板上。 这可以假设,如果你离开一个物体,破坏移走的物体将是非常便宜的。

我在这里提出了一个destruction_service类,对你希望它破坏的对象类型进行模板化。 这可以是任何类型的对象,而不仅仅是共享指针。 实际上,共享指针甚至是最棘手的,因为你必须要小心,如果它的引用计数已达到1,你只需要提交std::shared_ptr进行销毁。 否则,在销毁线程上销毁std::shared_ptr基本上是一个无操作,除了减少引用计数。 但是,在这种情况下,没有什么事情会发生。 你最终只会破坏一个不应该这样做的线程上的对象,因此可能被阻塞的时间超过了理想值。 对于调试,您可以在析构函数中assert您不在主线程上。

不过,我会要求该类型具有非抛出析构函数并移动构造运算符。

destruction_service<T>维护要被破坏的对象的std::vector<T> 提交一个破坏对象的push_back() s就在那个向量上。 工作线程等待队列变为非空,然后用它自己的空std::vector<T> swap() 离开临界区后,它clear()了矢量,从而破坏了所有物体。 向量本身保持不变,因此下次可以swap()减少对动态内存分配的需求。 如果您担心std::vector s从不收缩,请考虑使用std::deque 我不会使用std::list因为它为每个项目分配内存,分配内存来销毁对象有些悖论。 使用std::list作为工作队列的一般优点是你不必在关键部分分配内存,但是破坏对象可能是一个低优先级的任务,我不在乎工作线程是否被阻塞只要主线程保持响应,就比需要的时间长一点。 在C ++中没有标准的方法来设置线程的优先级,但是如果你愿意的话,你可以尝试通过std::threadnative_handle (在destruction_service的构造destruction_service )给予工作线程一个低优先级,给定你的平台允许这个。

destroy_service的destruction_servicejoin()工作线程。 如上所述,该类不可复制且不可移动。 如果需要移动它,请将其放入智能指针中。

#include <cassert>             // assert
#include <condition_variable>  // std::condition_variable
#include <mutex>               // std::mutex, std::lock_guard, std::unique_lock
#include <thread>              // std::thread
#include <type_traits>         // std::is_nothrow_{move_constructible,destructible}
#include <utility>             // std::move
#include <vector>              // std::vector


template <typename T>
class destruction_service final
{

  static_assert(std::is_nothrow_move_constructible<T>::value,
                "The to-be-destructed object needs a non-throwing move"
                " constructor or it cannot be safely delivered to the"
                " destruction thread");

  static_assert(std::is_nothrow_destructible<T>::value,
                "I'm a destruction service, not an ammunition disposal"
                " facility");

public:

  using object_type = T;

private:

  // Worker thread destroying objects.
  std::thread worker_ {};

  // Mutex to protect the object queue.
  mutable std::mutex mutex_ {};

  // Condition variable to signal changes to the object queue.
  mutable std::condition_variable condvar_ {};

  // Object queue of to-be-destructed items.
  std::vector<object_type> queue_ {};

  // Indicator that no more objects will be scheduled for destruction.
  bool done_ {};

public:

  destruction_service()
  {
    this->worker_ = std::thread {&destruction_service::do_work_, this};
  }

  ~destruction_service() noexcept
  {
    {
      const std::lock_guard<std::mutex> guard {this->mutex_};
      this->done_ = true;
    }
    this->condvar_.notify_all();
    if (this->worker_.joinable())
      this->worker_.join();
    assert(this->queue_.empty());
  }

  void
  schedule_destruction(object_type&& object)
  {
    {
      const std::lock_guard<std::mutex> guard {this->mutex_};
      this->queue_.push_back(std::move(object));
    }
    this->condvar_.notify_all();
  }

private:

  void
  do_work_()
  {
    auto things = std::vector<object_type> {};
    while (true)
      {
        {
          auto lck = std::unique_lock<std::mutex> {this->mutex_};
          if (this->done_)
            break;
          this->condvar_.wait(lck, [this](){ return !queue_.empty() || done_; });
          this->queue_.swap(things);
        }
        things.clear();
      }
    // By now, we may safely modify `queue_` without holding a lock.
    this->queue_.clear();
  }

};

这是一个简单的用例:

#include <atomic>   // std::atomic_int
#include <thread>   // std::this_thread::{get_id,yield}
#include <utility>  // std::exchange

#include "destruction_service.hxx"


namespace /* anonymous */
{

  std::atomic_int example_count {};
  std::thread::id main_thread_id {};

  class example
  {

  private:

    int id_ {-1};

  public:

    example() : id_ {example_count.fetch_add(1)}
    {
      std::this_thread::yield();
    }

    example(const example& other) : id_ {other.id_}
    {
    }

    example(example&& other) noexcept : id_ {std::exchange(other.id_, -1)}
    {
    }

    ~example() noexcept
    {
      assert(this->id_ < 0 || std::this_thread::get_id() != main_thread_id);
      std::this_thread::yield();
    }

  };

}  // namespace /* anonymous */


int
main()
{
  main_thread_id = std::this_thread::get_id();
  destruction_service<example> destructor {};
  for (int i = 0; i < 12; ++i)
    {
      auto thing = example {};
      destructor.schedule_destruction(std::move(thing));
    }
}

感谢Barry 审核此代码并提出一些改进建议。 请参阅我在Code Review上的问题,以获得一个不太简洁的代码版本,但没有包含他的建议。

A不应该对m_pFoo目标的破坏负责。 资源的破坏,其中shared_ptr点是有责任shared_ptr ,所以在我心里,你不应该微观管理上内部发生的真实对象的破坏线程~A

而不是实现一种适合您需求的新型智能指针,我认为这里的一个很好的折衷方案是将与底层对象相关的逻辑从~A移除并将其移动到您提供给shared_ptr的自定义删除器中。在施工。 如果您对目前的解除分配策略感到满意,我认为这是一种令人满意的方法。 但我同意其他人的意见,你可能想要研究不涉及为每次释放创建新线程的策略。

您可以在此处找到有关如何为smart_ptr提供删除器的smart_ptr 向下滚动到'带有删除器的构造函数'(您也可能需要查找文档以了解您正在使用的特定版本的boost)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM