![](/img/trans.png)
[英]How do I safely share a variable with a thread that is in a different compilation unit?
[英]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::thread
的native_handle
(在destruction_service
的构造destruction_service
)给予工作线程一个低优先级,给定你的平台允许这个。
destroy_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
提供删除器的smart_ptr
。 向下滚动到'带有删除器的构造函数'(您也可能需要查找文档以了解您正在使用的特定版本的boost)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.