这是线程安全队列类的正确方法吗?

Posted

技术标签:

【中文标题】这是线程安全队列类的正确方法吗?【英文标题】:Is this the right approach for a thread-safe Queue class? 【发布时间】:2009-10-18 02:31:03 【问题描述】:

我想知道这是否是用 C++ 编写线程安全队列的正确方法?

template <class T>
class Queue

public:

Queue() 

void Push(T& a)

    m_mutex.lock();
    m_q.push_back(a);
    m_mutex.unlock();


T& Pop()

    m_mutex.lock();
    T& temp = m_q.pop();
    m_mutex.unlock();
    return temp;


private:
    std::queue<t> m_q;
    boost::mutex m_mutex;
;

你明白了......我只是想知道这是否是最好的方法。谢谢!

编辑: 由于我遇到的问题,我想澄清一下互斥锁是一个 boost::mutex

【问题讨论】:

【参考方案1】:

我建议使用the Boost threading libraries 来帮助您。

你的代码很好,只是当你用 C++ 编写代码时

some_mutex.lock();
// do something
some_mutex.unlock();

那么如果// do something 部分中的代码抛出异常,那么锁将永远不会被释放。 Boost 库通过它的类(例如lock_guard)解决了这个问题,您可以在其中初始化一个对象,该对象在其构造函数中获取锁,而其析构函数释放锁。这样你就知道你的锁总是会被释放的。其他语言通过 try/finally 语句完成此操作,但 C++ 不支持此构造。

特别是,当您尝试从没有元素的队列中读取时会发生什么?这会引发异常吗?如果是这样,那么您的代码就会遇到问题。

当尝试获取第一个元素时,您可能想检查是否存在某些东西,如果没有,则进入睡眠状态并等到某些东西存在。这是 condition 对象的作业,同样由 Boost 库提供,但如果您愿意,可以在较低级别使用。

【讨论】:

很好地解决了一个非常不明显的问题。 作为一名 C# 程序员,这似乎很明显。 像 C++ 程序员一样使用异常。【参考方案2】:

Herb Sutter 去年在 Dobbs 博士杂志上写了一封 excellent article,涵盖了线程安全、无锁、单生产者、单消费者队列实现的所有主要问题。 (对上个月发布的实现进行了更正。)

他在下一期的 followup article 解决了多用户并发队列的更通用方法,并全面讨论了潜在的陷阱和性能问题。

有类似并发主题的a few more articles。

享受吧。

【讨论】:

【参考方案3】:

从线程的角度来看,这看起来很适合简单的线程安全队列。

不过,您确实有一个问题:std::queue 的 pop() 不会返回从队列中弹出的元素。你需要做的是:

T Pop()

    m_mutex.lock();
    T temp = m_q.front();
    m_q.pop();
    m_mutex.unlock();
    return temp;

在这种情况下,您不想返回引用,因为被引用的元素正在从队列中弹出并被销毁。

你还需要有一些公共 Size() 函数来告诉你队列中有多少元素(或者你需要优雅地处理调用 Pop() 并且没有元素的情况)队列)。

编辑:尽管正如 Eli Courtwright 指出的那样,您必须小心队列操作引发异常,并且使用 Boost 是一个好主意。

【讨论】:

谢谢... T temp = m_q.front() 可以是 T& temp = ... 只是为了节省另一个副本 ctor 吗?谢谢 否,因为调用 pop() 时引用的对象会被销毁。 但是为什么它起作用了哈哈?我刚刚试了一下,得到了有效的数据……这没有意义。 如果T 有一个微不足道的析构函数,它可能看起来 像这样有效,但实际上并不有效。当你调用m_q.pop()时,会弹出前面的元素并调用它的析构函数。 是的,在阅读了更多内容之后,你是对的......参考仍然让我感到困惑 :) 我将不得不承担复制 ctor 的成本。【参考方案4】:

取决于你的目标是什么。以这种方式制作,您将让您的“阅读器”客户端阻止您的“作者”客户端。您可能需要考虑使用“条件”来避免死锁等。

【讨论】:

我没有说有死锁:我说至少有一个限制(可能屈服于死锁),具体取决于使用模式。 什么使用模式?我计划将其与阅读器/编写器(每个)一起使用 那你打算制作你的“阅读器”吗?如果您调用“pop”,它将阻塞直到....?你的作者没有机会写作……除非你没有向我们展示你打算使用的所有代码。 我最初并不打算阻止流行音乐。如果我这样做了,我会使用一个条件变量......我本来打算在弹出之前调用 Empty() ......但这取决于竞争条件。所以,我必须重新考虑这个问题:(不过谢谢你提出这个问题。【参考方案5】:

您尝试实施的方法是锁定方法。它会起作用,除了如果你使用一个普通的系统提供的“互斥”对象,它的性能可能会令人失望(它的锁定解锁开销非常高)。很难说好不好,因为我们不知道您的性能要求和期望是什么。

由于您在代码的“锁定”段中执行的操作相当快,因此使用自旋锁代替真正的互斥锁或两者的组合可能是有意义的。这将为您提供更好的性能。再说一次,也许您的“互斥锁”已经实现了所有这些(无法知道,因为您没有提供有关该名称背后实际隐藏的内容的详细信息)。

最后,如果您碰巧正在寻找最佳性能,您可能需要阅读无锁同步方法,这是一个完全不同的故事。不过,无锁方法通常更难实现。

【讨论】:

【参考方案6】:

正如 Jean-Lou Dupont 所指出的,您当前的代码很容易出现死锁。完成此操作后,我使用了三个锁:两个​​计数信号量和一个互斥锁。计数的信号量在以下情况下发出信号:

    有空间可用于插入对象 至少有一个对象要从队列中检索
互斥锁仅在实际将项目放入队列或从队列中检索项目时使用 - 但在我们知道插入或检索将能够立即成功之前,不会尝试锁定互斥锁。

【讨论】:

以上是关于这是线程安全队列类的正确方法吗?的主要内容,如果未能解决你的问题,请参考以下文章

c++ string线程安全吗

SKNode线程安全吗?

正确关闭线程池:shutdown 和 shutdownNow 的区别

C#:高效的线程安全队列ConcurrentQueue<T>

这段代码安全吗,可以从构造函数 C++ 生成线程吗?

我可以使用 boost::threadpool 作为“线程安全队列”吗?