在不同的线程上安全地释放资源

Safely release a resource on a different thread

本文关键字:安全 释放资源 线程      更新时间:2023-10-16

我有一个类似的类:

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,您不在主线程上。

我要求该类型具有非抛出析构函数和move构造操作符。

一个destruction_service<T>维护着一个std::vector<T>,其中包含要被销毁的对象。提交对象销毁push_back()将其放在该向量上。工作线程等待队列变为非空,然后swap()用它自己的空std::vector<T>代替它。在离开临界区后,它将clear()作为向量,从而销毁所有对象。vector本身被保留下来,这样它就可以在下一次被swap()删除,从而减少对动态内存分配的需求。如果你担心std::vector不会缩水,可以考虑使用std::deque。我会避免使用std::list,因为它为每个项目分配内存,并且分配内存来销毁对象有点矛盾。使用std::list作为工作队列的通常优点是,你不必在临界区分配内存,但销毁对象可能是一个低优先级的任务,我不关心工作线程是否阻塞比需要的时间长一点,只要主线程保持响应。在c++中没有标准的方法来设置线程的优先级,但是如果你想,你可以尝试通过std::threadnative_handle(在destruction_service的构造函数中)给工作线程一个低优先级,如果你的平台允许这样做。

destruction_service的析构函数将join()工作线程。正如所写的,类是不可复制和不可移动的。如果你需要移动它,把它放在智能指针里。

#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提供删除器的文档。向下滚动到"带deleter的构造器"(您也可能想要查找您正在使用的boost的特定版本的文档)。