我正在努力实现线程安全的引用计数队列。我的想法是,我有许多任务,每个任务维护拥有队列的任务管理器的shared_ptr
。这是应该遇到相同问题的最小实现:
#include <condition_variable>
#include <deque>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
namespace {
class TaskManager;
struct Task {
std::function<void()> f;
std::shared_ptr<TaskManager> manager;
};
class Queue {
public:
Queue()
: _queue()
, _mutex()
, _cv()
, _running(true)
, _thread([this]() { sweepQueue(); })
{
}
~Queue() { close(); }
void close() noexcept
{
try {
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_running) {
return;
}
_running = false;
}
_cv.notify_one();
_thread.join();
} catch (...) {
std::cerr << "An error occurred while closing the queue\n";
}
}
void push(Task&& task)
{
std::unique_lock<std::mutex> lock(_mutex);
_queue.emplace_back(std::move(task));
lock.unlock();
_cv.notify_one();
}
private:
void sweepQueue() noexcept
{
while (true) {
try {
std::unique_lock<std::mutex> lock(_mutex);
_cv.wait(lock, [this] { return !_running || !_queue.empty(); });
if (!_running && _queue.empty()) {
return;
}
if (!_queue.empty()) {
const auto task = _queue.front();
_queue.pop_front();
task.f();
}
} catch (...) {
std::cerr << "An error occurred while sweeping the queue\n";
}
}
}
std::deque<Task> _queue;
std::mutex _mutex;
std::condition_variable _cv;
bool _running;
std::thread _thread;
};
class TaskManager : public std::enable_shared_from_this<TaskManager> {
public:
void addTask(std::function<void()> f)
{
_queue.push({ f, shared_from_this() });
}
private:
Queue _queue;
};
} // anonymous namespace
int main(void)
{
const auto manager = std::make_shared<TaskManager>();
manager->addTask([]() { std::cout << "Hello world\n"; });
}
我发现的问题是,在极少数情况下,队列将尝试在
sweepQueue
方法中调用其自己的析构函数。经过进一步检查,似乎最后一个任务出队后,TaskManager上的引用计数为零。如何在不调用析构函数的情况下安全地维护引用计数?更新:该示例未阐明
std::shared_ptr<TaskManager>
中是否需要Task
。这是一个用例示例,该用例应说明此看似不必要的所有权周期的必要性。std::unique_ptr<Task> task;
{
const auto manager = std::make_shared<TaskManager>();
task = std::make_unique<Task>(someFunc, manager);
}
// Guarantees manager is not destroyed while task is still in scope.
最佳答案
这里的所有权层次结构是TaskManager
拥有Queue
,而Queue
拥有Tasks
。 Tasks
维护指向TaskManager
的共享指针会创建一个所有权周期,这似乎在这里没有用处。
这是所有权的根本原因。 Queue
由TaskManager
拥有,因此Queue
可以具有指向TaskManager
的普通指针,并将该指针传递给Task
中的sweepQueue
。您完全不需要在std::shared_pointer<TaskManager>
中的Task
。