如何将 std::queue 封装成线程安全的队列

Posted on 2023-04-20 16:43  lyc2002  阅读(189)  评论(0编辑  收藏  举报

仅使用互斥保护每个成员函数

问题 1

尽管运用互斥保护共享数据,条件竞争仍然无法避免,考虑如下代码:

if (!q.empty()) {
    const int value = q.front();
    q.pop();
    do_something(value);
}

在 empty() 和 front() 之间可能有另一个线程调用 pop(),导致 front() 空队列调用

解决办法

front() 空队列调用抛出异常

缺点

  • 使用起来麻烦,需要将整个 if 语句置于 try 块中
  • 如果队列本身为空,调用 front() 肯定抛出异常,if 语句成了优化手段,而非必要设计

问题 2

A B
if (!q.empty())
if (!q.empty())
const int value = q.front()
const int value = q.front()
q.pop()
do_something(value) q.pop()
do_something(value)

队首元素被读取两次,但队首的第二个元素始终未被读取就被丢弃

解决办法

把 front() 和 pop() 组成一个成员函数

缺点

为了安全,常修改 front() 的实现使其返回副本而非引用。考虑 queue<vector< int>>,当把 front() 和 pop() 组成一个成员函数时,先调用 front(),假如系统负载过重或内存资源严重受限,内存分配可能失败,导致 vector 的拷贝构造函数抛出异常,但是 pop() 调用会使元素从栈上移除从而造成数据丢失。原接口的设计者将操作一分为二,即使无法安全地复制数据,数据还是留存在栈上。

最终解决办法

  • 传入引用 / 返回指向弹出元素的指针
  • 提供不抛出异常的拷贝构造函数或不抛出异常的移动构造函数

注意

代码可使用 std::move() 进行优化

无限队列代码

#include <queue>
#include <mutex>
#include <condition_variable>

template <typename T>
class threadsafe_queue
{
private:
    mutable std::mutex mut;
    std::condition_variable data_cond;
    std::queue<T> data_queue;

public:
    threadsafe_queue() {}

    void push(T new_value)
    {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(new_value);
        data_cond.notify_one();
    }

    bool try_pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return false;
        value = data_queue.front();
        data_queue.pop();
        return true;
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return false;
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    void wait_and_pop(T &value)
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]
                       { return !data_queue.empty(); });
        value = data_queue.front();
        data_queue.pop();
    }

    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]
                       { return !data_queue.empty(); });
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

有限队列代码

#include <queue>
#include <mutex>
#include <condition_variable>

template <typename T>
class threadsafe_queue
{
private:
    mutable std::mutex mut;
    std::condition_variable not_empty, not_full; // 变量名指示线程不阻塞的条件
    size_t max_sz;
    std::queue<T> data_queue;

public:
    threadsafe_queue(size_t sz) : max_sz(sz) {}

    bool try_push(T new_value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.size() >= max_sz) // 最好底层数据结构有 full 函数,会看起来更统一
            return false;
        data_queue.push(new_value);
        not_empty.notify_one();
        return true;
    }

    void wait_and_push(T new_value)
    {
        std::unique_lock<std::mutex> lk(mut);
        not_full.wait(lk, [this]
                      { return data_queue.size() < max_sz; });
        data_queue.push(new_value);
        not_empty.notify_one();
    }

    bool try_pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return false;
        value = data_queue.front();
        data_queue.pop();
        not_full.notify_one();
        return true;
    }

    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return false;
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        not_full.notify_one();
        return res;
    }

    void wait_and_pop(T &value)
    {
        std::unique_lock<std::mutex> lk(mut);
        not_empty.wait(lk, [this]
                       { return !data_queue.empty(); });
        value = data_queue.front();
        data_queue.pop();
        not_full.notify_one();
    }

    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        not_empty.wait(lk, [this]
                       { return !data_queue.empty(); });
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        not_full.notify_one();
        return res;
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};